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 an F
type functor taking Args
type constructor? And why it uses forwarding reference Args&&
instead of simply Args
?
With respect to 2, as for decltype(isValidImpl<decltype(f), decltype(args)&&...>(nullptr)){}
, I also cannot understand why it passes forwarding reference decltype(args)&&
instead of simply decltype(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 for hasFirst
trait) All I can find about void
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)
F is a function object callable with Args...
For the sake of mental model, picture std::declval<F>()
as a "fully constructed object of type F". std::declval
is there just in case F is not default-constructible and still needs to be used in unevaluated contexts.
For a default-constructible type this would be equivalent:
F()(std::declval<Args&&>()...);
In essence it's a call to F's constructor and then call to its operator()
with forwarded Args
. But imagine one type is constructible with int, another one is default-constructible, yet another one requires a string. Without some unevaluated constructor-like metafunction it would be impossible to cover all those cases.
You can read more on that in Alexandrescu's Modern C++ Design: Generic Programming and Design Patterns Applied.
Adding &&
to the argument type is effectively perfect-forwarding it. It may look obscure, but it's just a shorthand for decltype(std::forward<decltype(args)>(args))
. See the implementation of std::forward
and reference collapsing rules for more details. Keep in mind though, that this snippet adds rvalue-reference that collapses to the correct one when combined with the original type, not a forwarding one.
As it was stated in the comments: the type is not really needed, possibilty exists it cannot be returned, its presence there is just to check expression's correctness, afterwards it can be discarded.