I want to provide a to_string(obj)
function for every object type I create.
I found this question, applied the accepted answer, and it works. So far so good.
Then I created a new type, but forgot to write a to_string()
for it (or better: I accidentally made it unreachable by ADL). The problem is: my program still compiles fine, and at runtime I get an obscure stack overflow(TM).
Is there a way to obtain a reasonable error message, instead?
Here is a small program to demonstrate the problem: an infinite recursion between notstd::to_string()
and notstd::adl_helper::as_string()
.
#include <iostream>
#include <string>
namespace notstd {
namespace adl_helper {
using std::to_string;
template<class T>
std::string as_string( T&& t ) {
return to_string( std::forward<T>(t) );
}
}
template<class T>
std::string to_string( T&& t ) {
std::cout << "called" << std::endl; // <-- this is to show what's going on
return adl_helper::as_string(std::forward<T>(t));
}
class A {
/* both versions are needed, or the perfect forwarding candidate will
* always be chosen by the compiler in case of a non-perfect match */
//friend std::string to_string(A &a) { return std::string("a"); }
//friend std::string to_string(const A &a) { return std::string("a"); }
};
}
int main(int argc, char** argv) {
notstd::A a;
std::cout << to_string(a) << std::endl;
}
I tried creating a wrapper function that accepts one more parameter, to be used to perform the an anti-recursion check, like this:
#include <iostream>
#include <string>
#include <cassert>
namespace notstd {
namespace wrap_std {
std::string to_string(double v, bool) { return std::to_string(v); }
/* .... etc..... */
}
namespace adl_helper {
using wrap_std::to_string;
template<class T>
std::string as_string( T&& t ) {
return to_string( std::forward<T>(t), true );
}
}
template<class T>
std::string to_string( T&& t, bool recurring = false ) {
std::cout << "called" << std::endl;
assert(!recurring);
return adl_helper::as_string(std::forward<T>(t));
}
class A {
/* both versions are needed, or the perfect forwarding candidate will
* always be chosen by the compiler in case of a non-perfect match */
//friend std::string to_string(A &a) { return std::string("A"); }
//friend std::string to_string(const A &a) { return std::string("A"); }
};
}
int main(int argc, char** argv) {
notstd::A a;
std::cout << to_string(a) << std::endl;
}
The problems here are:
Maybe I could use a template to wrap std::to_string()
and create specializations for my types... this would be a quite different beast, but at least it would provide a compile time error if a suitable specialization is not available. I would have, again, to wrap all std::to_string()
overloads, and I'd probably have to (almost) forget about ADL, at least until c++20 is supported by all compilers, If I understand well.
Does anyone have a better solution?
Thanks!
The idea of that accepted answer is different: you put A
outside notstd
namespace and then use qualified notstd::to_string
instead of unqualified to_string
. That is:
namespace notstd {
// ...
}
class A {
friend std::string to_string(const A&);
};
A a;
std::cout << notstd::to_string(a);
Now your code won't compile if there is no friend
function. Moreover, you need only one friend function (taking const A&
), because notstd::to_string(T&&)
won't be present in the overload set inside adl_helper::as_string(T&&)
.
Putting A
inside notstd
screws everything up. You have infinite recursion problem and you need two friends to handle both A
and const A
cases in the presence of notstd::to_string(T&&)
candidate: if only one friend is defined, that candidate is a better match in one of the cases because const
qualifier should be added/dropped to invoke the friend function.