I am reading some examples of SFINAE-based traits, but unable to make sense out of the one related to generic lambdas in C++17 (isvalid.hpp).
I can understand that it roughly contains some major parts in order to implement a type trait such as isDefaultConstructible
or hasFirst
trait (isvalid1.cpp):
1. Helper functions using SFINAE technique:
#include <type_traits>
// helper: checking validity of f(args...) for F f and Args... args:
template<typename F, typename... Args,
typename = decltype(std::declval<F>()(std::declval<Args&&>()...))>
std::true_type isValidImpl(void*);
// fallback if helper SFINAE'd out:
template<typename F, typename... Args>
std::false_type isValidImpl(...);
2. Generic lambda to determine the validity:
// define a lambda that takes a lambda f and returns whether calling f with args is valid
inline constexpr
auto isValid = [](auto f) {
return [](auto&&... args) {
return decltype(isValidImpl<decltype(f),
decltype(args)&&...
>(nullptr)){};
};
};
3. Type helper template:
// helper template to represent a type as a value
template<typename T>
struct TypeT {
using Type = T;
};
// helper to wrap a type as a value
template<typename T>
constexpr auto type = TypeT<T>{};
// helper to unwrap a wrapped type in unevaluated contexts
template<typename T>
T valueT(TypeT<T>); // no definition needed
4. Finally, compose them into isDefaultConstructible
trait to check whether a type is default constructible:
constexpr auto isDefaultConstructible
= isValid([](auto x) -> decltype((void)decltype(valueT(x))()) {
});
It is used like this (Live Demo):
struct S {
S() = delete;
};
int main() {
std::cout << std::boolalpha;
std::cout << "int: " << isDefaultConstructible(type<int>) << std::endl; // true
std::cout << "int&: " << isDefaultConstructible(type<int&>) << std::endl; // false
std::cout << "S: " << isDefaultConstructible(type<S>) << std::endl; // false
return 0;
}
However, some of the syntax are so complicated and I cannot figure out.
My questions are:
With respect to 1, as for
std::declval<F>()(std::declval<Args&&>()...)
, does it mean that it is anF
type functor takingArgs
type constructor? And why it uses forwarding referenceArgs&&
instead of simplyArgs
?With respect to 2, as for
decltype(isValidImpl<decltype(f), decltype(args)&&...>(nullptr)){}
, I also cannot understand why it passes forwarding referencedecltype(args)&&
instead of simplydecltype(args)
?With respect to 4, as for
decltype((void)decltype(valueT(x))())
, what is the purpose of(void)
casting here? ((void)
casting can also be found in isvalid1.cpp forhasFirst
trait) All I can find aboutvoid
casting is Casting to void to avoid use of overloaded user-defined Comma operator, but it seems it is not the case here.
Thanks for any insights.
P.S. For one who wants more detail could check C++ Templates: The Complete Guide, 2nd - 19.4.3 Using Generic Lambdas for SFINAE. The author also mentioned that some of the techniques are used widely in Boost.Hana, so I also listen to Louis Dionne's talk about it. Yet, it only helps me a little to understand the code snippet above. (It is still a great talk about the evolution of C++ metaprogramming)