License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright :
Frédéric Pennerath,
CentraleSupelec
Last modified : April 19, 2024 10:22
Link to the source : advanced.md
C++11/C++14 offer new very powerful features for meta-programming thanks to SFINAE and type traits.
Given a function template with several possible specializations, SFINAE allows to select which specialization should be instantiated using evolved criteria:
// is_deref(const T&) is true if T::operator*() exists.
constexpr bool is_deref(...) { return false; } // By default operator* is not expected to exist
// decltype generates an error if *t is not defined.
template <typename T>
constexpr auto is_deref(const T& t) -> decltype(*t, true) { return true; }
...
int val = 1;
int* ptr = &val;
std::cout << "is_deref(val) = " << is_deref(val) << std::endl; // Prints false
std::cout << "is_deref(ptr) = " << is_deref(ptr) << std::endl; // Prints true
type_traits
)std::enable_if
is a very practical tool when using SFINAE with type traits:// Definition of enable_if in header <type_traits>
template<bool cond, typename T = void>
struct enable_if {
// Default definition defines an empty struct that generates
};
template<class T>
struct enable_if<true, T> {
typedef T type; // Type alias only declared when passed constexpr condition is true
};
// Example of usage of enable_if
template<typename T>
auto my_function(const T& t) -> typename std::enable_if<condition on T, int>::type { // Tries to access alias ::type (undefined if condition is false)
// Function instanciated only if T satisfies the condition
}
std::enable_if
and std::result_of
// If type Callable returns void
template<typename Callable>
typename std::enable_if<
std::is_void<
typename std::result_of<Callable>::type
>::value,
void
>::type
operator() (const Callable& f){
auto start = my_clock::now();
f();
auto end = my_clock::now();
duration += (end - start);
}
// If Callable returns a type not void
template<typename Callable>
typename std::enable_if<
! std::is_void<
typename std::result_of<Callable>::type
>::value,
typename std::result_of<Callable>::type
>::type
operator() (const Callable& f) {
auto start = my_clock::now();
auto res = f();
auto end = my_clock::now();
duration += (end - start);
return res;
}
This allows to instrument code as:
void f1() { ... }
}
int f2() { ... }
...
f1(); int res = f2();
//
Chrono chrono;
chrono(f1); int res = chrono(f2);
A variadic template is a template (of a class or function) with a variable number of arguments. |
The standard library generalizes the class template std::pair
to an arbitrary number of components through the variadic class template std::tuple
:
std::pair
and std::tuple
#include <utility> // Contains definition of std::pair
std::pair<int,bool> p1(3,true);
std::cout << "Value of p1 : " << p1.first << ',' << p1.second << std::endl;
auto p2 = std::make_pair(1.1f, "one");
#include <tuple> // Contains definition of std::tuple
std::tuple<int,bool,char,MyClass> t1(3,true,'c',MyClass());
std::cout << std::get<0,int,bool,char,MyClass>(t1);
// Or simply
std::cout << std::get<1>(t1) std::endl;
auto t2 = std::make_tuple(1.1f, "one", 'c'); // Before C++17
std::tuple t2 = { 1.1f, "one", 'c' }; // From C++17
// Retrieving components by structure binding operation
float x;
const char* s;
char c;
std::tie(x, s, c) = t2; // Before C++17
auto [x, s, c] = t2; // From C++17
std::tuple
(simplified version)// Declaration of a variadic template
template <typename... Elems> struct Tuple;
// Recursive specialization of the template
template <typename Head, typename... Rest>
struct Tuple<Head, Rest...> {
Head first; // First component
Tuple<Rest...> rest; // Rest of components
Tuple() : first(), rest() {} // Default constructor
};
// Terminal specialization of the template
template <>
struct Tuple<> {
Tuple() {}
};
// Let's instantiate it.
Tuple<int, double, char> t;
The code above is equivalent to:
class Tuple<int, double, char> {
int first;
Tuple<double, char> rest;
};
class Tuple<double, char> {
double first;
Tuple<char> rest;
};
class Tuple<char> {
char first;
Tuple<> rest;
};
class Tuple<> {
};
Tuple
is first declared without being defined. It is then specialized twice: once for the instances of Tuple
with at least one type as an argument, and once for the instances without any type (i.e empty tuple).typename... Rest
refers to the operation of parameter packing. Symbol Rest
is called parameter pack and represents an arbitrary list of types.typename Rest...
refers to the operation of parameter unpacking for parameter pack Rest
. It is called expansion parameter pack and represents the list of types captured by Rest
during parameter packing.A function applied to a variadic class template requires to be recursive. Let’s consider the example of the constructor of previous class Tuple
:
// Declaration of variadic class template
template <typename... Elems> struct Tuple;
// Recursive specialization of the constructor
template <typename Head, typename... Rest>
struct Tuple<Head, Rest...> {
Head first; // First component
Tuple<Rest...> rest; // Rest of components
...
Tuple(const Head& h, const Rest&... r) : first(h), rest(r...) {}
};
// Terminal specialization
template <> struct Tuple<> {
Tuple() {}
};
const
, lvalue or value references, etc. Example const Rest&...
r...
refers to the list of values mapped to types in Rest...
operator<<
for class Tuple:// Forward declaration of class Tuple
template <typename... Elems> struct Tuple;
template<typename Head, typename... Rest>
std::ostream& operator<<(std::ostream& os, const Tuple<Head, Rest...>& t) {
std::cout << t.first << ',' << t.rest;
return os;
}
// Terminal recursion
std::ostream& operator<<(std::ostream& os, const Tuple<>& t) {
return os;
}
// To avoid an extra coma
template<typename Head>
std::ostream& operator<<(std::ostream& os, const Tuple<Head>& t) {
std::cout << t.first;
return os;
}
template <typename Head, typename... Rest>
struct Tuple<Head, Rest...> {
...
// Declare both non empty operator<< as friends
template<typename, typename...>
friend std::ostream& operator<<(std::ostream&, const Tuple<Head, Rest...>&);
template<typename>
friend std::ostream& operator<<(std::ostream&, const Tuple<Head, Rest...>&);
...
};
std::get
on variadic class template Tuple
We will construct std::get
step by step so that in the end, get
returns the ith component, e.g
std::get<1>(std::make_tuple(1,'a',true)); // Returns 'a'
get
template<int index, typename... Elements> auto get(Tuple<Elements...>& t) {
// To complete
}
template<int index, typename Head, typename... Rest>
struct TupleIter {
static auto get(Tuple<Head, Rest...>& t) {
// To complete
}
};
We can now complete code of get
:
template<int index, typename... Elements> auto get(Tuple<Elements...>& t) {
return TupleIter<index, Elements...>::get(t);
}
Class TupleIter
must be friend of class Tuple
:
template <typename Head, typename... Rest>
class Tuple<Head, Rest...> {
Head first;
Tuple<Rest...> rest;
...
template<int,typename, typename...> friend struct TupleIter;
}
get
in the general casetemplate<int index, typename Head, typename... Rest>
struct TupleIter {
static auto get(Tuple<Head, Rest...>& t) {
return TupleIter<index - 1, Rest...>::get(t.rest);
}
};
template<typename Head, typename... Rest>
struct TupleIter<0, Head, Rest...> {
static Head get(Tuple<Head, Rest...>& t) {
return t.first;
}
};
Fold expressions facilitates the definition of a unary or binary operator to be applied to a parameter pack. |
There exist 4 types of fold expressions as defined in the next table where:
args
refers to a value pack a1, ..., an
. f
is a function (or function template) * Op
is a binary operator. * v0
is an initial value.
Fold Type | Folded form | Unfolded form |
---|---|---|
Unary right |
(f(args) Op ...)
|
f(a1) Op (f(a2) Op ( ... f(an)))))
|
Unary left |
(... Op f(args))
|
((((f(a1) Op f(a2)) ... ) Op f(an)
|
Binary right |
(f(args) Op ... v0)
|
f(a1) Op (f(a2) Op ( ... (f(an) Op v0)))))
|
Binary left |
(v0 ... Op f(args))
|
((((v0 Op f(a1)) ... ) Op f(an)
|
Here are two examples:
operator,
template<typename Func, typename... Args>
void map(Func f, const Args&... args) {
(f(args), ...);
}
...
int i = 0;
auto f = [&i] (auto& v) { std::cout << ++i << ") " << v << std::endl; };
map(f, 3, "abcd", 1.);
The resulting output is:
1) 3
2) abcd
3) 1
operator+
template<typename I, typename... Values>
auto sum_of_squares(const I& init, const Values&... values) {
return ((init * init) + ... + (values * values));
}
...
std::cout << sum_of_squares(true, 2., 3) << std::endl; // Print 14 as 1^2 + 2^2 + 3^2 = 14