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!

davnat
  • 51
  • 1
  • 6

1 Answers1

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.

Evg
  • 25,259
  • 5
  • 41
  • 83
  • So, based on this idea, I should add `to_string(std::vector)` and `to_string(std::array)` to `std` namespace? – davnat Jun 04 '20 at 21:23
  • @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;`. – Evg Jun 05 '20 at 07:20
  • This makes sense, I think the second option is the best way to go. Thanks, again. – davnat Jun 05 '20 at 11:19