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?