0

I am a fresher to C++ template and just learning SFINAE and relating features in C++.

Background and the code

I am reading a post from here. In that post, author tries to implement boost hana's is_valid, which is used to test whether an object satisfying some property.

The functionality to implement here is quite simply: If I can call .serialze() on the obj, then call it. Otherwise, fall-back to use to_string().

The final code I imitate from that post is:

template <typename UnnamedType> struct Container
{
// Let's put the test in private.
private:
    // We use std::declval to 'recreate' an object of 'UnnamedType'.
    // We use std::declval to also 'recreate' an object of type 'Param'.
    // We can use both of these recreated objects to test the validity!
    template <typename Param> constexpr auto testValidity(int /* unused */)
    -> decltype(std::declval<UnnamedType>()(std::declval<Param>()), std::true_type{})
    {
        // If substitution didn't fail, we can return a true_type.
        return std::true_type{};
    }

    template <typename Param> constexpr std::false_type testValidity(...)
    {
        // Our sink-hole returns a false_type.
        return std::false_type{};
    }

public:
    // A public operator() that accept the argument we wish to test onto the UnnamedType.
    // Notice that the return type is automatic!
    template <typename Param> constexpr auto operator()(Param&&)
    {
        // The argument is forwarded to one of the two overloads.
        // The SFINAE on the 'true_type' will come into play to dispatch.
        // Once again, we use the int for the precedence.
        return testValidity<Param>(int());
    }
};

auto l5 = [](auto&& t) -> decltype(std::forward<decltype(t)>(t).serialize()) { };
auto hasSerialize = Container<decltype(l5)>{};

template <class T> auto serialize(T&& obj) 
-> typename std::enable_if<decltype(hasSerialize(std::forward<T>(obj)))::value, std::string>::type
{
    return std::forward<T>(obj).serialize();
}

template <class T> auto serialize(T&& obj) 
-> typename std::enable_if<!decltype(hasSerialize(std::forward<T>(obj)))::value, std::string>::type
{
    return to_string(obj);
}

It's a little bit complicated because I try to deal with the rvalue argument with perfect forwarding.

Then I need a class to test:

class C {
 public:
  std::string serialize() && { return "This is C"; }  //NOTE: there is a ref-qualifier
};

std::string to_string(const C&) { return "This is C in to_string()"; }

int main() {
    C c{};
    std::cout << serialize(c) << std::endl;
}

Environment and Results

when I compile at MacOS (M1) with clang 13.0.0: clang++ -std=c++14 -O0 there is no compile error and the output is: "This is C in to_string()".

Question

Then my question is here: When resolve for the call of serialze(c) in the main, the compiler need to subsitite the serialize() function. Then need to instantiate for hasSerialize::operator() beacause it appears in the std::enable_if (in both serialize()).

In order to instantiate hasSerialize::operator() the compiler need to instantiate for testValidity<Param> where Param is C& because it appears in the function body.

Then the compiler will try to substitute for

template <typename Param> constexpr auto testValidity(int /* unused */)
    -> decltype(std::declval<UnnamedType>()(std::declval<Param>()), std::true_type{})`

Finally the compiler will try to subsititute for the generic lambda

[](auto&& t) -> decltype(std::forward<decltype(t)>(t).serialize()) { };

because it appers in the decltype at the return type of testValidity.

The subsitution failure happens there at the generic lambda, because the type of t is C&, but the .serialize() is only valid for C&&. The subsitution failure here is not in the immediate context (if I understanding the answer here correctly).

Why the compiler does not throw a hard error?

huangjl
  • 85
  • 1
  • 5

0 Answers0