Chapter 11

Generic Libraries

So far, our discussion of templates has focused on their specific features, capabilities, and constraints, with immediate tasks and applications in mind (the kind of things we run into as application programmers). However, templates are most effective when used to write generic libraries and frameworks, where our designs have to consider potential uses that are a priori broadly unconstrained. While just about all the content in this book can be applicable to such designs, here are some general issues you should consider when writing portable components that you intend to be usable for as-yet unimagined types.

The list of issues raised here is not complete in any sense, but it summarizes some of the features introduced so far, introduces some additional features, and refers to some features covered later in this book. We hope it will also be a great motivator to read through the many chapters that follow.

11.1 Callables

Many libraries include interfaces to which client code passes an entity that must be “called.” Examples include an operation that must be scheduled on another thread, a function that describes how to hash values to store them in a hash table, an object that describes an order in which to sort elements in a collection, and a generic wrapper that provides some default argument values. The standard library is no exception here: It defines many components that take such callable entities.

One term used in this context is callback. Traditionally that term has been reserved for entities that are passed as function call arguments (as opposed to, e.g., template arguments), and we maintain this tradition. For example, a sort function may include a callback parameter as “sorting criterion,” which is called to determine whether one element precedes another in the desired sorted order.

In C++, there are several types that work well for callbacks because they can both be passed as function call arguments and can be directly called with the syntax f():

• Pointer-to-function types

• Class types with an overloaded operator() (sometimes called functors), including lambdas

• Class types with a conversion function yielding a pointer-to-function or reference-to-function

Collectively, these types are called function object types, and a value of such a type is a function object. 157

The C++ standard library introduces the slightly broader notion of a callable type, which is either a function object type or a pointer to member. An object of callable type is a callable object, which we refer to as a callable for convenience.

Generic code often benefits from being able to accept any kind of callable, and templates make it possible to do so.

11.1.1 Supporting Function Objects

Let’s look how the for_each() algorithm of the standard library is implemented (using our own name “foreach” to avoid name conflicts and for simplicity skipping returning anything):

basics/foreach.hpp

template<typename Iter, typename Callable>
void foreach (Iter current, Iter end, Callable op)
{
   while (current != end) {        //as long as not reached the end
     op(*current);                 // call passed operator for current element
     ++current;                    // and move iterator to next element
   }
}

The following program demonstrates the use of this template with various function objects:

basics/foreach.cpp

#include <iostream>
#include <vector>
#include "foreach.hpp"

// a function to call:
void func(int i)
  { std::cout << "func() called for:  " << i << ’ ’;
}

// a function object type (for objects that can be used as functions):
class FuncObj {
  public:
    void operator() (int i) const {   //Note: const member function
      std::cout << "FuncObj::op() called for:      " << i << ’ ’;
    }
};

int main()
{
  std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
  foreach(primes.begin(), primes.end(),        // range
          func);                               // function as callable (decays to pointer)
  foreach(primes.begin(), primes.end(),        // range
          &func);                              // function pointer as callable
  foreach(primes.begin(), primes.end(),        // range
          FuncObj());                          // function object as callable
  foreach(primes.begin(), primes.end(),        // range
          [] (int i) {                         //lambda as callable
             std::cout << "lambda called for:      " << i << ’ ’;
            });
    }

Let’s look at each case in detail:

• When we pass the name of a function as a function argument, we don’t really pass the function itself but a pointer or reference to it. As with arrays (see Section 7.4 on page 115), function arguments decay to a pointer when passed by value, and in the case of a parameter whose type is a template parameter, a pointer-to-function type will be deduced.

Just like arrays, functions can be passed by reference without decay. However, function types cannot really be qualified with const. If we were to declare the last parameter of foreach() with type Callable const&, the const would just be ignored. (Generally speaking, references to functions are rarely used in mainstream C++ code.)

• Our second call explicitly takes a function pointer by passing the address of a function name. This is equivalent to the first call (where the function name implicitly decayed to a pointer value) but is perhaps a little clearer.

• When passing a functor, we pass a class type object as a callable. Calling through a class type usually amounts to invoking its operator(). So the call

op(*current);

is usually transformed into

op.operator()(*current);        // call operator() with parameter *current for op

Note that when defining operator(), you should usually define it as a constant member function. Otherwise, subtle error messages can occur when frameworks or libraries expect this call not to change the state of the passed object (see Section 9.4 on page 146 for details).

It is also possible for a class type object to be implicitly convertible to a pointer or reference to a surrogate call function (discussed in Section C.3.5 on page 694). In such a case, the call

op(*current);

would be transformed into

(op.operator F())(*current);

where F is the type of the pointer-to-function or reference-to-function that the class type object can be converted to. This is relatively unusual.

• Lambda expressions produce functors (called closures), and therefore this case is not different from the functor case. Lambdas are, however, a very convenient shorthand notation to introduce functors, and so they appear commonly in C++ code since C++11.

Interestingly, lambdas that start with [] (no captures) produce a conversion operator to a function pointer. However, that is never selected as a surrogate call function because it is always a worse match than the normal operator() of the closure.

11.1.2 Dealing with Member Functions and Additional Arguments

One possible entity to call was not used in the previous example: member functions. That’s because calling a nonstatic member function normally involves specifying an object to which the call is applied using syntax like object.memfunc() or ptr->memfunc() and that doesn’t match the usual pattern function-object().

Fortunately, since C++17, the C++ standard library provides a utility std::invoke() that conveniently unifies this case with the ordinary function-call syntax cases, thereby enabling calls to any callable object with a single form. The following implementation of our foreach() template uses std::invoke():

basics/foreachinvoke.hpp

#include <utility>
#include <functional>

template<typename Iter, typename Callable, typename… Args>
void foreach (Iter current, Iter end, Callable op, Args const&… args)
{
  while (current != end) {        //as long as not reached the end of the elements
    std::invoke(op,               //call passed callable with
            args…,              //any additional args
            *current);            // and the current element
    ++current;
  }
}

Here, besides the callable parameter, we also accept an arbitrary number of additional parameters. The foreach() template then calls std::invoke() with the given callable followed by the additional given parameters along with the referenced element. std::invoke() handles this as follows:

• If the callable is a pointer to member, it uses the first additional argument as the this object. All remaining additional parameters are just passed as arguments to the callable.

• Otherwise, all additional parameters are just passed as arguments to the callable.

Note that we can’t use perfect forwarding here for the callable or additional parameters: The first call might “steal” their values, leading to unexpected behavior calling op in subsequent iterations.

With this implementation, we can still compile our original calls to foreach() above. Now, in addition, we can also pass additional arguments to the callable and the callable can be a member function.1 The following client code illustrates this:

basics/foreachinvoke.cpp

#include <iostream>
#include <vector>
#include <string>
#include "foreachinvoke.hpp"

// a class with a member function that shall be called
class MyClass {
  public:
    void memfunc(int i) const {
      std::cout << "MyClass::memfunc() called for: " << i << ’ ’;  
    }
};

int main()
{
  std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };

  // pass lambda as callable and an additional argument:
  foreach(primes.begin(), primes.end(),           //elements for 2nd arg of lambda
          [](std::string const& prefix, int i) {  //lambda to call
            std::cout << prefix << i << ’ ’;
          },
          "- value:");                            //1st arg of lambda

  // call obj.memfunc() for/with each elements in primes passed as argument
  MyClass obj;
  foreach(primes.begin(), primes.end(),
   //elements used as args
          &MyClass::memfunc,              //member function to call
          obj);                           // object to call memfunc() for
}

The first call of foreach() passes its fourth argument (the string literal "- value: ") to the first parameter of the lambda, while the current element in the vector binds to the second parameter of the lambda. The second call passes the member function memfunc() as the third argument to be called for obj passed as the fourth argument.

See Section D.3.1 on page 716 for type traits that yield whether a callable can be used by std::invoke().

11.1.3 Wrapping Function Calls

A common application of std::invoke() is to wrap single function calls (e.g., to log the calls, measure their duration, or prepare some context such as starting a new thread for them). Now, we can support move semantics by perfect forwarding both the callable and all passed arguments:

basics/invoke.hpp

#include <utility>           // for std::invoke()
#include <functional>        // for std::forward()

template<typename Callable, typename… Args>
decltype(auto) call(Callable&& op, Args&&… args)
{
  return std::invoke(std::forward<Callable>(op),        //passed callable with
                     std::forward<Args>(args)…);      // any additional args
}

The other interesting aspect is how to deal with the return value of a called function to “perfectly forward” it back to the caller. To support returning references (such as a std::ostream&) you have to use decltype(auto) instead of just auto:

template<typename Callable, typename… Args>
decltype(auto) call(Callable&& op, Args&&… args)

decltype(auto) (available since C++14) is a placeholder type that determines the type of variable, return type, or template argument from the type of the associated expression (initializer, return value, or template argument). See Section 15.10.3 on page 301 for details.

If you want to temporarily store the value returned by std::invoke() in a variable to return it after doing something else (e.g., to deal with the return value or log the end of the call), you also have to declare the temporary variable with decltype(auto):

decltype(auto) ret{std::invoke(std::forward<Callable>(op),
                                std::forward<Args>(args)…)};

return ret;

Note that declaring ret with auto&& is not correct. As a reference, auto&& extends the lifetime of the returned value until the end of its scope (see Section 11.3 on page 167) but not beyond the return statement to the caller of the function.

However, there is also a problem with using decltype(auto): If the callable has return type void, the initialization of ret as decltype(auto) is not allowed, because void is an incomplete type. You have the following options:

• Declare an object in the line before that statement, whose destructor performs the observable behavior that you want to realize. For example:2

struct cleanup {
  ~cleanup() {
    …  //code to perform on return
  }
} dummy;
return std::invoke(std::forward<Callable>(op),
                   std::forward<Args>(args)…);

• Implement the void and non-void cases differently:

basics/invokeret.hpp

#include <utility>           // for std::invoke()
#include <functional>        // for std::forward()
#include <type_traits>       // for std::is_same<> and invoke_result<>

template<typename Callable, typename… Args>
decltype(auto) call(Callable&& op, Args&&… args)
{
  if constexpr(std::is_same_v<std::invoke_result_t<Callable, Args…>,
                              void>) {
    // return type is void:
    std::invoke(std::forward<Callable>(op),
                std::forward<Args>(args)…);

    …
    return;
  }
  else {
    // return type is not void:
    decltype(auto) ret{std::invoke(std::forward<Callable>(op),
                       std::forward<Args>(args)…)};
    …
    return ret;
  }
}

With

    if constexpr(std::is_same_v<std::invoke_result_t<Callable, Args…>,
                                void>)

we test at compile time whether the return type of calling callable with Args… is void. See Section D.3.1 on page 717 for details about std::invoke_result<>.3

Future C++ versions might hopefully avoid the need for such as special handling of void (see Section 17.7 on page 361).

11.2 Other Utilities to Implement Generic Libraries

std::invoke() is just one example of useful utilities provided by the C++ standard library for implementing generic libraries. In what follows, we survey some other important ones.

11.2.1 Type Traits

The standard library provides a variety of utilities called type traits that allow us to evaluate and modify types. This supports various cases where generic code has to adapt to or react on the capabilities of the types for which they are instantiated. For example:

#include <type_traits>

template<typename T>
class C
{
  // ensure that T is not void (ignoring const or volatile):
  static_assert(!std::is_same_v<std::remove_cv_t<T>,void>,
                "invalid instantiation of class C for void type");
  public:
    template<typename V>
    void f(V&& v) {
      if constexpr(std::is_reference_v<T>) {
        …  // special code if T is a reference type
      }
      if constexpr(std::is_convertible_v<std::decay_t<V>,T>) {
        …  // special code if V is convertible to T
      }
      if constexpr(std::has_virtual_destructor_v<V>) {
        …  // special code if V has virtual destructor
      }
   }
};

As this example demonstrates, by checking certain conditions we can choose between different implementations of the template. Here, we use the compile-time if feature, which is available since C++17 (see Section 8.5 on page 134), but we could have used std::enable_if, partial specialization, or SFINAE to enable or disable helper templates instead (see Chapter 8 for details).

However, note that type traits must be used with particular care: They might behave differently than the (naive) programmer might expect. For example:

std::remove_const_t<int const&>    // yields int const&

Here, because a reference is not const (although you can’t modify it), the call has no effect and yields the passed type.

As a consequence, the order of removing references and const matters:

std::remove_const_t<std::remove_reference_t<int const&>>        // int std::remove_reference_t<std::remove_const_t<int const&>>        // int const

Instead, you might call just

std::decay_t<int const&>        // yields int

which, however, would also convert raw arrays and functions to the corresponding pointer types.

Also there are cases where type traits have requirements. Not satisfying those requirements results in undefined behavior.4 For example:

make_unsigned_t<int>               // unsigned int
make_unsigned_t<int const&>        // undefined behavior (hopefully error)

Sometimes the result may be surprising. For example:

add_rvalue_reference_t<int>              // int&&
add_rvalue_reference_t<int const>        // int const&&
add_rvalue_reference_t<int const&>       // int const& (lvalue-ref remains lvalue-ref)

Here we might expect that add_rvalue_reference always results in an rvalue reference, but the reference-collapsing rules of C++ (see Section 15.6.1 on page 277) cause the combination of an lvalue reference and rvalue reference to produce an lvalue reference.

As another example:

is_copy_assignable_v<int>        // yields true (generally, you can assign an int to an int)
is_assignable_v<int,int>         // yields false (can’t call 42 = 42)

While is_copy_assignable just checks in general whether you can assign ints to another (checking the operation for lvalues), is_assignable takes the value category (see Appendix B) into account (here checking whether you can assign a prvalue to a prvalue). That is, the first expression is equivalent to

is_assignable_v<int&,int&>        // yields true

For the same reason:

is_swappable_v<int>               // yields true (assuming lvalues)
is_swappable_v<int&,int&>         // yields true (equivalent to the previous check)
is_swappable_with_v<int,int>      // yields false (taking value category into account)

For all these reasons, carefully note the exact definition of type traits. We describe the standard ones in detail in Appendix D.

11.2.2 std::addressof()

The std::addressof<>() function template yields the actual address of an object or function. It works even if the object type has an overloaded operator &. Even though the latter is somewhat rare, it might happen (e.g., in smart pointers). Thus, it is recommended to use addressof() if you need an address of an object of arbitrary type:

template<typename T>
void f (T&& x)
{
  auto p = &x;        // might fail with overloaded operator &
  auto q = std::addressof(x);        // works even with overloaded operator &
  …
}

11.2.3 std::declval()

The std::declval<>() function template can be used as a placeholder for an object reference of a specific type. The function doesn’t have a definition and therefore cannot be called (and doesn’t create an object). Hence, it can only be used in unevaluated operands (such as those of decltype and sizeof constructs). So, instead of trying to create an object, you can assume you have an object of the corresponding type.

For example, the following declaration deduces the default return type RT from the passed template parameters T1 and T2:

basics/maxdefaultdeclval.hpp

#include <utility>

template<typename T1, typename T2,
         typename RT = std::decay_t<decltype(true ? std::declval<T1>()
                                                  : std::declval<T2>())>>
RT max (T1 a, T2 b)
{
  return b < a ? a : b;
}

To avoid that we have to call a (default) constructor for T1 and T2 to be able to call operator ?: in the expression to initialize RT, we use std::declval to “use” objects of the corresponding type without creating them. This is only possible in the unevaluated context of decltype, though.

Don’t forget to use the std::decay<> type trait to ensure the default return type can’t be a reference, because std::declval() itself yields rvalue references. Otherwise, calls such as max(1, 2) will get a return type of int&&.5 See Section 19.3.4 on page 415 for details.

11.3 Perfect Forwarding Temporaries

As shown in Section 6.1 on page 91, we can use forwarding references and std::forward<> to “perfectly forward” generic parameters:

template<typename T>
void f (T&& t)                  // t is forwarding reference
{
  g(std::forward<T>(t));        // perfectly forward passed argument t to g()
}

However, sometimes we have to perfectly forward data in generic code that does not come through a parameter. In that case, we can use auto&& to create a variable that can be forwarded. Assume, for example, that we have chained calls to functions get() and set() where the return value of get() should be perfectly forwarded to set():

template<typename T>
void foo(T x)
{
  set(get(x));
}

Suppose further that we need to update our code to perform some operation on the intermediate value produced by get(). We do this by holding the value in a variable declared with auto&&:

template<typename T>
void foo(T x)
{
  auto&& val = get(x);
  …
  // perfectly forward the return value of get() to set():
  set(std::forward<decltype(val)>(val));
}

This avoids extraneous copies of the intermediate value.

11.4 References as Template Parameters

Although it is not common, template type parameters can become reference types. For example:

basics/tmplparamref.cpp

#include <iostream>

template<typename T>
void tmplParamIsReference(T) {
  std::cout << "T is reference: " <<  std::is_reference_v<T> << ’ ’;
}
int main()
{
  std::cout << std::boolalpha;
  int i;
  int& r = i;
  tmplParamIsReference(i);        // false
  tmplParamIsReference(r);        // false
  tmplParamIsReference<int&>(i);  // true
  tmplParamIsReference<int&>(r);  // true
}

Even if a reference variable is passed to tmplParamIsReference(), the template parameter T is deduced to the type of the referenced type (because, for a reference variable v, the expression v has the referenced type; the type of an expression is never a reference). However, we can force the reference case by explicitly specifying the type of T:

tmplParamIsReference<int&>(r);
tmplParamIsReference<int&>(i);

Doing this can fundamentally change the behavior of a template, and, as likely as not, a template may not have been designed with this possibility in mind, thereby triggering errors or unexpected behavior. Consider the following example:

basics/referror1.cpp

template<typename T, T Z = T{}>
class RefMem {
  private:
    T zero;
  public:
    RefMem() : zero{Z} {
    }
};


int null = 0;

int main()
{
    RefMem<int> rm1, rm2;
    rm1 = rm2;
             // OK

    RefMem<int&> rm3;      // ERROR: invalid default value for N
    RefMem<int&, 0> rm4;
   // ERROR: invalid default value for N

    extern int null;
    RefMem<int&,null> rm5, rm6;
    rm5 = rm6;
             // ERROR: operator= is deleted due to reference member
}

Here we have a class with a member of template parameter type T, initialized with a nontype template parameter Z that has a zero-initialized default value. Instantiating the class with type int works as expected. However, when trying to instantiate it with a reference, things become tricky:

• The default initialization no longer works.

• You can no longer pass just 0 as initializer for an int.

• And, perhaps most surprising, the assignment operator is no longer available because classes with nonstatic reference members have deleted default assignment operators.

Also, using reference types for nontype template parameters is tricky and can be dangerous. Consider this example:

basics/referror2.cpp

#include <vector>
#include <iostream>

template<typename T, int& SZ>      // Note: size is reference
class Arr {
  private:
    std::vector<T> elems;
  public:
    Arr() : elems(SZ) {            //use current SZ as initial vector size
    }
    void print() const {
      for (int i=0; i<SZ; ++i) {   //loop over SZ elements
        std::cout << elems[i] << ’ ’;
      }
    }
};

int size = 10;

int main()
{
  Arr<int&,size> y;        // compile-time ERROR deep in the code of class std::vector<>
  Arr<int,size> x;         // initializes internal vector with 10 elements
  x.print();               // OK
  size += 100;             // OOPS: modifies SZ in Arr<>
  x.print();
               // run-time ERROR: invalid memory access: loops over 120 elements
}

Here, the attempt to instantiate Arr for elements of a reference type results in an error deep in the code of class std::vector<>, because it can’t be instantiated with references as elements:

Arr<int&,size> y;        // compile-time ERROR deep in the code of class std::vector<>

The error often leads to the “error novel” described in Section 9.4 on page 143, where the compiler provides the entire template instantiation history from the initial cause of the instantiation down to the actual template definition in which the error was detected.

Perhaps worse is the run-time error resulting from making the size parameter a reference: It allows the recorded size value to change without the container being aware of it (i.e., the size value can become invalid). Thus, operations using the size (like the print() member) are bound to run into undefined behavior (causing the program to crash, or worse):

int int size = 10;

Arr<int,size> x;  // initializes internal vector with 10 elements
size += 100;      // OOPS: modifies SZ in Arr<>
x.print();
        // run-time ERROR: invalid memory access: loops over 120 elements

Note that changing the template parameter SZ to be of type int const& does not address this issue, because size itself is still modifiable.

Arguably, this example is far-fetched. However, in more complex situations, issues like these do occur. Also, in C++17 nontype parameters can be deduced; for example:

template<typename T, decltype(auto) SZ>
class Arr;

Using decltype(auto) can easily produce reference types and is therefore generally avoided in this context (use auto by default). See Section 15.10.3 on page 302 for details.

The C++ standard library for this reason sometimes has surprising specifications and constraints. For example:

• In order to still have an assignment operator even if the template parameters are instantiated for references, classes std::pair<> and std::tuple<> implement the assignment operator instead of using the default behavior. For example:

namespace std {
  template<typename T1, typename T2>
  struct pair {
    T1 first;
    T2 second;
    …
    // default copy/move constructors are OK even with references:
    pair(pair const&) = default;
    pair(pair&&) = default;
    …
    // but assignment operator have to be defined to be available with references:
    pair& operator=(pair const& p);
    pair& operator=(pair&& p) noexcept(…);
    …
  };
}

• Because of the complexity of possible side effects, instantiation of the C++17 standard library class templates std::optional<> and std::variant<> for reference types is ill-formed (at least in C++17).

To disable references, a simple static assertion is enough:

template<typename T>
class optional
{
  static_assert(!std::is_reference<T>::value,
                "Invalid instantiation of optional<T> for references");
  …
};

Reference types in general are quite unlike other types and are subject to several unique language rules. This impacts, for example, the declaration of call parameters (see Section 7 on page 105) and also the way we define type traits (see Section 19.6.1 on page 432).

11.5 Defer Evaluations

When implementing templates, sometimes the question comes up whether the code can deal with incomplete types (see Section 10.3.1 on page 154). Consider the following class template:

template<typename T>
class Cont {
  private:
    T* elems;
  public:
    …
};

So far, this class can be used with incomplete types. This is useful, for example, with classes that refer to elements of their own type:

struct Node
{
    std::string value;
    Cont<Node> next;        // only possible if Cont accepts incomplete types
};

However, for example, just by using some traits, you might lose the ability to deal with incomplete types. For example:

template<typename T>
class Cont {
  private:
    T* elems;
  public:
    …
  typename std::conditional<std::is_move_constructible<T>::value,
                            T&&,
                            T&
                           >::type
  foo();
};

Here, we use the trait std::conditional (see Section D.5 on page 732) to decide whether the return type of the member function foo() is T&& or T&. The decision depends on whether the template parameter type T supports move semantics.

The problem is that the trait std::is_move_constructible requires that its argument is a complete type (and is not void or an array of unknown bound; see Section D.3.2 on page 721). Thus, with this declaration of foo(), the declaration of struct node fails.6

We can deal with this problem by replacing foo() by a member template so that the evaluation of std::is_move_constructible is deferred to the point of instantiation of foo():

template<typename T>
class Cont {
  private:
    T* elems;
  public:
    template<typename D = T>
    typename std::conditional<std::is_move_constructible<D>::value,
                              T&&,
                              T&
                             >::type
    foo();
};

Now, the traits depends on the template parameter D (defaulted to T, the value we want anyway) and the compiler has to wait until foo() is called for a concrete type like Node before evaluating the traits (by then Node is a complete type; it was only incomplete while being defined).

11.6 Things to Consider When Writing Generic Libraries

Let’s list some things to remember when implementing generic libraries (note that some of them might be introduced later in this book):

• Use forwarding references to forward values in templates (see Section 6.1 on page 91). If the values do not depend on template parameters, use auto&& (see Section 11.3 on page 167).

• When parameters are declared as forwarding references, be prepared that a template parameter has a reference type when passing lvalues (see Section 15.6.2 on page 279).

• Use std::addressof() when you need the address of an object depending on a template parameter to avoid surprises when it binds to a type with overloaded operator& (Section 11.2.2 on page 166).

• For member function templates, ensure that they don’t match better than the predefined copy/move constructor or assignment operator (Section 6.4 on page 99).

• Consider using std::decay when template parameters might be string literals and are not passed by value (Section 7.4 on page 116 and Section D.4 on page 731).

• If you have out or inout parameters depending on template parameters, be prepared to deal with the situation that const template arguments may be specified (see, e.g., Section 7.2.2 on page 110).

• Be prepared to deal with the side effects of template parameters being references (see Section 11.4 on page 167 for details and Section 19.6.1 on page 432 for an example). In particular, you might want to ensure that the return type can’t become a reference (see Section 7.5 on page 117).

• Be prepared to deal with incomplete types to support, for example, recursive data structures (see Section 11.5 on page 171).

• Overload for all array types and not just T[SZ] (see Section 5.4 on page 71).

11.7 Summary

• Templates allow you to pass functions, function pointers, function objects, functors, and lambdas as callables.

• When defining classes with an overloaded operator(), declare it as const (unless the call changes its state).

• With std::invoke(), you can implement code that can handle all callables, including member functions.

• Use decltype(auto) to forward a return value perfectly.

• Type traits are type functions that check for properties and capabilities of types.

• Use std::addressof() when you need the address of an object in a template.

• Use std::declval() to create values of specific types in unevaluated expressions.

• Use auto&& to perfectly forward objects in generic code if their type does not depend on template parameters.

• Be prepared to deal with the side effects of template parameters being references.

• You can use templates to defer the evaluation of expressions (e.g., to support using incomplete types in class templates).

1 std::invoke() also allows a pointer to data member as a callback type. Instead of calling a function, it returns the value of the corresponding data member in the object referred to by the additional argument.

2 Thanks to Daniel Krügler for pointing that out.

3 std::invoke_result<> is available since C++17. Since C++11, to get the return type you could call: typename std::result_of<Callable(Args…)>::type

4 There was a proposal for C++17 to require that violations of preconditions of type traits must always result in a compile-time error. However, because some type traits have over-constraining requirements, such as always requiring complete types, this change was postponed.

5 Thanks to Dietmar Kühl for pointing that out.

6 Not all compilers yield an error if std::is_move_constructible is not an incomplete type. This is allowed, because for this kind of error, no diagnostics is required. Thus, this is at least a portability problem.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.122.162