Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
CPlusPlusNotesForProfessionals.pdf
Скачиваний:
47
Добавлен:
20.05.2023
Размер:
5.11 Mб
Скачать

Chapter 103: SFINAE (Substitution Failure Is Not An Error)

Section 103.1: What is SFINAE

SFINAE stands for Substitution Failure Is Not An Error. Ill-formed code that results from substituting types (or values) to instantiate a function template or a class template is not a hard compile error, it is only treated as a deduction failure.

Deduction failures on instantiating function templates or class template specializations remove that candidate from the set of consideration - as if that failed candidate did not exist to begin with.

template <class T>

auto begin(T& c) -> decltype(c.begin()) { return c.begin(); }

template <class T, size_t N>

T* begin(T (&arr)[N]) { return arr; }

int vals[10];

begin(vals); // OK. The first function template substitution fails because

//vals.begin() is ill-formed. This is not an error! That function

//is just removed from consideration as a viable overload candidate,

//leaving us with the array overload.

Only substitution failures in the immediate context are considered deduction failures, all others are considered hard errors.

template <class T>

void add_one(T& val) { val += 1; }

int i = 4; add_one(i); // ok

std::string msg = "Hello";

add_one(msg); // error. msg += 1 is ill-formed for std::string, but this // failure is NOT in the immediate context of substituting T

Section 103.2: void_t

Version ≥ C++11

void_t is a meta-function that maps any (number of) types to type void. The primary purpose of void_t is to facilitate writing of type traits.

std::void_t will be part of C++17, but until then, it is extremely straightforward to implement:

template <class...> using void_t = void;

Some compilers require a slightly di erent implementation:

template <class...>

struct make_void { using type = void; };

template <typename... T>

using void_t = typename make_void<T...>::type;

GoalKicker.com – C++ Notes for Professionals

513

The primary application of void_t is writing type traits that check validity of a statement. For example, let's check if a type has a member function foo() that takes no arguments:

template <class T, class=void> struct has_foo : std::false_type {};

template <class T>

struct has_foo<T, void_t<decltype(std::declval<T&>().foo())>> : std::true_type {};

How does this work? When I try to instantiate has_foo<T>::value, that will cause the compiler to try to look for the best specialization for has_foo<T, void>. We have two options: the primary, and this secondary one which involves having to instantiate that underlying expression:

If T does have a member function foo(), then whatever type that returns gets converted to void, and the specialization is preferred to the primary based on the template partial ordering rules. So has_foo<T>::value will be true

If T doesn't have such a member function (or it requires more than one argument), then substitution fails for the specialization and we only have the primary template to fallback on. Hence, has_foo<T>::value is false.

A simpler case:

template<class T, class=void>

struct can_reference : std::false_type {};

template<class T>

struct can_reference<T, std::void_t<T&>> : std::true_type {};

this doesn't use std::declval or decltype.

You may notice a common pattern of a void argument. We can factor this out:

struct details {

template<template<class...>class Z, class=void, class...Ts> struct can_apply:

std::false_type {};

template<template<class...>class Z, class...Ts> struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:

std::true_type {};

};

template<template<class...>class Z, class...Ts> using can_apply = details::can_apply<Z, void, Ts...>;

which hides the use of std::void_t and makes can_apply act like an indicator whether the type supplied as the first template argument is well-formed after substituting the other types into it. The previous examples may now be rewritten using can_apply as:

template<class T> using ref_t = T&;

template<class T>

 

using can_reference = can_apply<ref_t, T>;

// Is T& well formed for T?

and:

GoalKicker.com – C++ Notes for Professionals

514

template<class

T>

 

using dot_foo_r = decltype(std::declval<T&>().foo());

template<class

T>

 

using can_dot_foo = can_apply< dot_foo_r, T >;

// Is T.foo() well formed for T?

 

 

 

which seems simpler than the original versions.

There are post-C++17 proposals for std traits similar to can_apply.

The utility of void_t was discovered by Walter Brown. He gave a wonderful presentation on it at CppCon 2016.

Section 103.3: enable_if

std::enable_if is a convenient utility to use boolean conditions to trigger SFINAE. It is defined as:

template <bool Cond, typename Result=void> struct enable_if { };

template <typename Result> struct enable_if<true, Result> {

using type = Result;

};

That is, enable_if<true, R>::type is an alias for R, whereas enable_if<false, T>::type is ill-formed as that specialization of enable_if does not have a type member type.

std::enable_if can be used to constrain templates:

int negate(int i) { return -i; }

template <class F>

auto negate(F f) { return -f(); }

Here, a call to negate(1) would fail due to ambiguity. But the second overload is not intended to be used for integral types, so we can add:

int negate(int i) { return -i; }

template <class F, class = typename std::enable_if<!std::is_arithmetic<F>::value>::type> auto negate(F f) { return -f(); }

Now, instantiating negate<int> would result in a substitution failure since !std::is_arithmetic<int>::value is false. Due to SFINAE, this is not a hard error, this candidate is simply removed from the overload set. As a result, negate(1) only has one single viable candidate - which is then called.

When to use it

It's worth keeping in mind that std::enable_if is a helper on top of SFINAE, but it's not what makes SFINAE work in the first place. Let's consider these two alternatives for implementing functionality similar to std::size, i.e. an overload set size(arg) that produces the size of a container or array:

// for containers template<typename Cont>

auto size1(Cont const& cont) -> decltype( cont.size() );

GoalKicker.com – C++ Notes for Professionals

515

// for arrays

template<typename Elt, std::size_t Size> std::size_t size1(Elt const(&arr)[Size]);

//implementation omitted template<typename Cont> struct is_sizeable;

//for containers

template<typename Cont, std::enable_if_t<std::is_sizeable<Cont>::value, int> = 0> auto size2(Cont const& cont);

// for arrays

template<typename Elt, std::size_t Size> std::size_t size2(Elt const(&arr)[Size]);

Assuming that is_sizeable is written appropriately, these two declarations should be exactly equivalent with respect to SFINAE. Which is the easiest to write, and which is the easiest to review and understand at a glance?

Now let's consider how we might want to implement arithmetic helpers that avoid signed integer overflow in favour of wrap around or modular behaviour. Which is to say that e.g. incr(i, 3) would be the same as i += 3 save for the fact that the result would always be defined even if i is an int with value INT_MAX. These are two possible alternatives:

//handle signed types template<typename Int>

auto incr1(Int& target, Int amount)

-> std::void_t<int[static_cast<Int>(-1) < static_cast<Int>(0)]>;

//handle unsigned types by just doing target += amount

//since unsigned arithmetic already behaves as intended template<typename Int>

auto incr1(Int& target, Int amount)

-> std::void_t<int[static_cast<Int>(0) < static_cast<Int>(-1)]>;

template<typename Int, std::enable_if_t<std::is_signed<Int>::value, int> = 0> void incr2(Int& target, Int amount);

template<typename Int, std::enable_if_t<std::is_unsigned<Int>::value, int> = 0> void incr2(Int& target, Int amount);

Once again which is the easiest to write, and which is the easiest to review and understand at a glance?

A strength of std::enable_if is how it plays with refactoring and API design. If is_sizeable<Cont>::value is meant to reflect whether cont.size() is valid then just using the expression as it appears for size1 can be more concise, although that could depend on whether is_sizeable would be used in several places or not. Contrast that with std::is_signed which reflects its intention much more clearly than when its implementation leaks into the declaration of incr1.

Section 103.4: is_detected

To generalize type_trait creation:based on SFINAE there are experimental traits detected_or, detected_t, is_detected.

With template parameters typename Default, template <typename...> Op and typename ... Args:

is_detected: alias of std::true_type or std::false_type depending of the validity of Op<Args...>

detected_t: alias of Op<Args...> or nonesuch depending of validity of Op<Args...>.

GoalKicker.com – C++ Notes for Professionals

516

detected_or: alias of a struct with value_t which is is_detected, and type which is Op<Args...> or Default

depending of validity of Op<Args...>

which can be implemented using std::void_t for SFINAE as following:

Version ≥ C++17

namespace detail {

template <class Default, class AlwaysVoid, template<class...> class Op, class... Args>

struct detector

{

using value_t = std::false_type; using type = Default;

};

template <class Default, template<class...> class Op, class... Args> struct detector<Default, std::void_t<Op<Args...>>, Op, Args...>

{

using value_t = std::true_type; using type = Op<Args...>;

};

} // namespace detail

// special type to indicate detection failure struct nonesuch {

nonesuch() = delete; ~nonesuch() = delete;

nonesuch(nonesuch const&) = delete;

void operator=(nonesuch const&) = delete;

};

template <template<class...

> class

Op, class...

Args>

using

is_detected =

 

 

 

typename detail::detector<nonesuch, void, Op, Args...>::value_t;

template <template<class...

> class

Op, class...

Args>

using

detected_t = typename detail::detector<nonesuch, void, Op, Args...>::type;

template <class Default, template<class...> class Op, class... Args> using detected_or = detail::detector<Default, void, Op, Args...>;

Traits to detect presence of method can then be simply implemented:

typename <typename T, typename ...Ts>

using foo_type = decltype(std::declval<T>().foo(std::declval<Ts>()...));

struct C1 {};

struct C2 {

int foo(char) const;

};

template <typename T>

using has_foo_char = is_detected<foo_type, T, char>;

static_assert(!has_foo_char<C1>::value, "Unexpected"); static_assert(has_foo_char<C2>::value, "Unexpected");

static_assert(std::is_same<int, detected_t<foo_type, C2, char>>::value, "Unexpected");

GoalKicker.com – C++ Notes for Professionals

517