SpECTRE  v2024.09.29
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Modules Pages
SFINAE

SFINAE, Substitution Failure Is Not An Error, means that if a deduced template substitution fails, compilation must continue. This can be exploited to make decisions at compile time. See here for a discussion using std::enable_if to remove certain functions from overload resolution or certain template specializations from name lookup. Another method of controlling name lookup resolution is using std::void_t. void_t is a metafunction from types to void, that is

template <typename... Args>
using void_t = void;

void_t is useful when used in combination with decltype and std::declval to probe if a type has certain members. For example, we can implement a type trait to check if a type T is iterable by first have the general definition inherit from std::false_type as follows,

template <typename T, typename = void>
struct is_iterable : std::false_type {};

Next we will have specialization that uses void_t to check if the type T has a begin() and end() function.

template <typename T>
struct is_iterable<T, std::void_t<decltype(std::declval<T>().begin(),
std::declval<T>().end())>>

What is happening here? Well, we use std::declval to convert the type T to a reference type, which allows us to call member functions inside decltype expressions without having to construct an object. First we try to call the member function begin() on std::declval<T>(), and if that succeeds we throw away the result using the comma operator. Next we try to call end() on std::declval<T>(), which, if it succeeds we get the return type of using decltype. Note that decltype is important because we can only call member functions on reference types inside of decltype, not just anywhere. Finally, if all this succeeded use void_t to metareturn void, otherwise the template parameters of void_t fail to evaluate and the specialization cannot be resolved during name lookup. We could just as well use

template <typename T>
struct is_iterable<T, std::void_t<decltype(std::declval<T>().begin()),
decltype(std::declval<T>().end())>>

Which of the two implementations of the is_iterable is preferred is simply a matter of taste, both behave correctly.

If you're reading closely you might wonder why the void_t is necessary at all, why not just decltype(...)? Well the reason is that since the default template parameter metavalue is void, the specialization cannot be resolved during name lookup unless the second template parameter in the specialization is either void as well or is explicitly specified when the class template is being invoked. Thus, the clearest implementation probably is

template <typename T, typename = std::void_t<>>
struct is_iterable : std::false_type {};
template <typename T>
struct is_iterable<T, std::void_t<decltype(std::declval<T>().begin(),
std::declval<T>().end())>>

You could now also define a helper type alias and constexpr boolean

template <typename T>
using is_iterable_t = typename is_iterable<T>::type;
template <typename T>
constexpr T & value(T &t)
Returns t.value() if t is a std::optional otherwise returns t.
Definition: OptionalHelpers.hpp:32
constexpr bool is_iterable_v
Definition: IsIterable.hpp:51
typename is_iterable< T >::type is_iterable_t
Definition: IsIterable.hpp:55