2

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:

  • I'd have to wrap all std::to_string() overloads
  • I'll only get a runtime error, but I feel the problem could and should be detected ad compile time
  • I'm probably adding some overhead, for something useful only during development: maybe I could add some macros to deactivate all this in release mode, but it would add even more work

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!

0

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

3 Comments

So, based on this idea, I should add to_string(std::vector) and to_string(std::array) to std namespace?
@davnat, you can't add anything into std, unless this is explicitly allowed by the standard. But you can add to_string(std::vector/array) into adl_helper. You can also add these functions into some other namespace (my), and then add using my::to_string; after using std::to_string;.
This makes sense, I think the second option is the best way to go. Thanks, again.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.