3

I just read the Boost.Hana tutorial but unfortunately got stuck very early. Could anybody explain to me why to_json for integers is implemented the way it is:

template <typename T>
auto to_json(T const& x) -> decltype(std::to_string(x)) {
  return std::to_string(x);
}

I thought that the return type would be simply equivalent to std::string but it is not. If you replace it with std::string the compiler complains about ambiguous function call. What is the difference between std::string and decltype(std::to_string(x))?

AndyG
  • 39,700
  • 8
  • 109
  • 143
Nils
  • 13,319
  • 19
  • 86
  • 108
  • 3
    What is this [tag:blackmagic] tag about? – Max Langhof Aug 08 '19 at 14:37
  • It is about black magic! – Nils Aug 08 '19 at 14:41
  • 2
    After thinking a bit about it I think it works the following way: If x is not an integral type (for which overloads of std::to_string(x) exist) the decltype expression is no longer valid and this causes substitution to fail. Therefore the compiler only selects this function for integral types. – Nils Aug 08 '19 at 14:44
  • @MaxLanghof It is kinda related but not exactly the same. – Nils Aug 08 '19 at 14:44
  • You are correct, this does indeed use SFINAE. The code in your question doesn't have any function overloading so that misled me, sorry. – Max Langhof Aug 08 '19 at 14:59
  • 1
    @Nils you've got it, it enables SFINAE. You can answer yourself if you wish :) – Quentin Aug 08 '19 at 15:00

1 Answers1

4

This is because SFINAE applies to the expression of the return type.

Not all types can be sent to std::to_string. This makes the expression of the return type resolve to a function that cannot be called with the provided argument. This is a subtitution failure and that triggers SFINAE and the canditate is discarded.

When changing the return type to std::string, then the overload is not discarded, even if std::to_string(x) would not compile, so the function still takes part in the overload set, making the call ambiguous.


There are other places you could put the constraint. Here is some examples:

template<typename T> // in the non traitling return type
decltype(constrait) to_json() {}

// in the template parameters
template<typename T, decltype(void(constraint), 0) = 0>
auto to_json() -> std::string {}

// (less common) in the function parameters
template<typename T>
auto to_json(decltype(void(constraint), 0) = 0) {}
Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • Thanks for your answer, I now understand it. But having auto as return type and then the alternative return type syntax with -> looks awkward, is this a common technique to leverage SFINAE? – Nils Aug 09 '19 at 05:50
  • 1
    @Nils See the `auto f() -> int` as an alternative syntax for declaration. In fact, it would be nice to have a `func` keyword to replace the auto: `func f() -> int`. There are places that it makes the syntax much simpler, for example in member function returning a member type, or to constraint function like your example. I use this style everywhere because I try to be consistent and all function names are aligned :) – Guillaume Racicot Aug 09 '19 at 13:35
  • Hah then we would basically have Swifts function declaration syntax! I wonder how overload resolution for generic functions works there... – Nils Aug 09 '19 at 14:11