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

auto some_args = std::make_tuple(42, 'x', 3.14);

int r = apply(f, some_args); // calls f(42, 'x', 3.14)

Section 16.4: Tag Dispatching

A simple way of selecting between functions at compile time is to dispatch a function to an overloaded pair of functions that take a tag as one (usually the last) argument. For example, to implement std::advance(), we can dispatch on the iterator category:

namespace details {

template <class RAIter, class Distance>

void advance(RAIter& it, Distance n, std::random_access_iterator_tag) { it += n;

}

template <class BidirIter, class Distance>

void advance(BidirIter& it, Distance n, std::bidirectional_iterator_tag) { if (n > 0) {

while (n--) ++it;

}

else {

while (n++) --it;

}

}

template <class InputIter, class Distance>

void advance(InputIter& it, Distance n, std::input_iterator_tag) { while (n--) {

++it;

}

}

}

template <class Iter, class Distance> void advance(Iter& it, Distance n) {

details::advance(it, n,

typename std::iterator_traits<Iter>::iterator_category{} );

}

The std::XY_iterator_tag arguments of the overloaded details::advance functions are unused function parameters. The actual implementation does not matter (actually it is completely empty). Their only purpose is to allow the compiler to select an overload based on which tag class details::advance is called with.

In this example, advance uses the iterator_traits<T>::iterator_category metafunction which returns one of the iterator_tag classes, depending on the actual type of Iter. A default-constructed object of the iterator_category<Iter>::type then lets the compiler select one of the di erent overloads of details::advance. (This function parameter is likely to be completely optimized away, as it is a default-constructed object of an empty struct and never used.)

Tag dispatching can give you code that's much easier to read than the equivalents using SFINAE and enable_if.

Note: while C++17's if constexpr may simplify the implementation of advance in particular, it is not suitable for open implementations unlike tag dispatching.

Section 16.5: Detect Whether Expression is Valid

It is possible to detect whether an operator or function can be called on a type. To test if a class has an overload of

GoalKicker.com – C++ Notes for Professionals

90

std::hash, one can do this:

#include <functional> // for std::hash

#include <type_traits> // for std::false_type and std::true_type #include <utility> // for std::declval

template<class, class = void> struct has_hash

: std::false_type

{};

template<class T>

struct has_hash<T, decltype(std::hash<T>()(std::declval<T>()), void())> : std::true_type

{};

Version ≥ C++17

Since C++17, std::void_t can be used to simplify this type of construct

#include <functional> // for std::hash

#include <type_traits> // for std::false_type, std::true_type, std::void_t #include <utility> // for std::declval

template<class, class = std::void_t<> > struct has_hash

: std::false_type

{};

template<class T>

struct has_hash<T, std::void_t< decltype(std::hash<T>()(std::declval<T>())) > > : std::true_type

{};

where std::void_t is defined as:

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

For detecting if an operator, such as operator< is defined, the syntax is almost the same:

template<class, class = void> struct has_less_than

: std::false_type

{};

template<class T>

struct has_less_than<T, decltype(std::declval<T>() < std::declval<T>(), void())> : std::true_type

{};

These can be used to use a std::unordered_map<T> if T has an overload for std::hash, but otherwise attempt to use a std::map<T>:

template <class K, class V>

using hash_invariant_map = std::conditional_t< has_hash<K>::value,

std::unordered_map<K, V>, std::map<K,V>>;

GoalKicker.com – C++ Notes for Professionals

91

Section 16.6: If-then-else

Version ≥ C++11

The type std::conditional in the standard library header <type_traits> can select one type or the other, based on a compile-time boolean value:

template<typename T> struct ValueOrPointer

{

typename std::conditional<(sizeof(T) > sizeof(void*)), T*, T>::type vop;

};

This struct contains a pointer to T if T is larger than the size of a pointer, or T itself if it is smaller or equal to a pointer's size. Therefore sizeof(ValueOrPointer) will always be <= sizeof(void*).

Section 16.7: Manual distinction of types when given any type T

When implementing SFINAE using std::enable_if, it is often useful to have access to helper templates that determines if a given type T matches a set of criteria.

To help us with that, the standard already provides two types analog to true and false which are std::true_type and std::false_type.

The following example show how to detect if a type T is a pointer or not, the is_pointer template mimic the behavior of the standard std::is_pointer helper:

template <typename T>

struct is_pointer_: std::false_type {};

template <typename T>

struct is_pointer_<T*>: std::true_type {};

template <typename T>

struct is_pointer: is_pointer_<typename std::remove_cv<T>::type> { }

There are three steps in the above code (sometimes you only need two):

1.The first declaration of is_pointer_ is the default case, and inherits from std::false_type. The default case should always inherit from std::false_type since it is analogous to a "false condition".

2.The second declaration specialize the is_pointer_ template for pointer T* without caring about what T is really. This version inherits from std::true_type.

3.The third declaration (the real one) simply remove any unnecessary information from T (in this case we remove const and volatile qualifiers) and then fall backs to one of the two previous declarations.

Since is_pointer<T> is a class, to access its value you need to either:

Use ::value, e.g. is_pointer<int>::value value is a static class member of type bool inherited from std::true_type or std::false_type;

Construct an object of this type, e.g. is_pointer<int>{} – This works because std::is_pointer inherits its default constructor from std::true_type or std::false_type (which have constexpr constructors) and both std::true_type and std::false_type have constexpr conversion operators to bool.

GoalKicker.com – C++ Notes for Professionals

92

It is a good habit to provides "helper helper templates" that let you directly access the value:

template <typename T>

constexpr bool is_pointer_v = is_pointer<T>::value;

Version ≥ C++17

In C++17 and above, most helper templates already provide a _v version, e.g.:

template< class T > constexpr bool is_pointer_v = is_pointer<T>::value; template< class T > constexpr bool is_reference_v = is_reference<T>::value;

Section 16.8: Calculating power with C++11 (and higher)

With C++11 and higher calculations at compile time can be much easier. For example calculating the power of a given number at compile time will be following:

template <typename T>

constexpr T calculatePower(T value, unsigned power) {

return power == 0 ? 1 : value * calculatePower(value, power-1);

}

Keyword constexpr is responsible for calculating function in compilation time, then and only then, when all the requirements for this will be met (see more at constexpr keyword reference) for example all the arguments must be known at compile time.

Note: In C++11 constexpr function must compose only from one return statement.

Advantages: Comparing this to the standard way of compile time calculation, this method is also useful for runtime calculations. It means, that if the arguments of the function are not known at the compilation time (e.g. value and power are given as input via user), then function is run in a compilation time, so there's no need to duplicate a code (as we would be forced in older standards of C++).

E.g.

void useExample() {

constexpr int compileTimeCalculated = calculatePower(3, 3); // computes at compile time,

//as both arguments are known at compilation time

//and used for a constant expression.

int value; std::cin >> value;

int runtimeCalculated = calculatePower(value, 3); // runtime calculated, // because value is known only at runtime.

}

Version ≥ C++17

Another way to calculate power at compile time can make use of fold expression as follows:

#include <iostream> #include <utility>

template <class T, T V, T N, class I = std::make_integer_sequence<T, N>> struct power;

template <class T, T V, T N, T... Is>

struct power<T, V, N, std::integer_sequence<T, Is...>> {

static constexpr T value = (static_cast<T>(1) * ... * (V * static_cast<bool>(Is + 1)));

};

GoalKicker.com – C++ Notes for Professionals

93