Core knowledge of C + + 11-17 template (XI) — basic technology for compiling generic library

  • Callables
    • Function objectsfunction objects
    • Handle member functions and additional parameters
      • std::invoke<>()
    • Unified packaging
  • Other basic techniques of generic Libraries
    • Type Traits
    • std::addressof()
    • std::declval
  • Perfect forwarding
  • Reference as template parameter
  • Defer evaluations


Many base libraries require the caller to pass a callable entity. For example, a function that describes how to sort and a function that how to hash. General usecallbackTo describe this usage. In C + +, there are several forms to implement callback, which can be passed as function parameters and can be used directlyf(...)Called as:

  • Pointer to the function.
  • Overloadedoperator()Class (sometimes calledfunctors), including Lambdas
  • Contains a class of conversion functions that can generate function pointers or function references.

C + + usecallable typeTo describe these types. For example, an object that can be called is calledcallable object, we usecallbackTo simplify the name.

Writing generic code is much more extensible because of this usage.

Function objectsfunction objects

For example, a for_ Implementation of each:

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

Use differentFunction ObjectsTo call this template:

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

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

int main(int argc, const char **argv) {
  std::vector primes = {2, 3, 5, 7, 11, 13, 17, 19};

  foreach (primes.begin(), primes.end(),  func);       // range function as callable (decays to pointer)
  foreach (primes.begin(), primes.end(), &func);         // range function pointer as callable

  foreach (primes.begin(), primes.end(), FuncObj());     // range function object as callable
  foreach (primes.begin(), primes.end(),     // range lambda as callable
           [](int i) {                   
             std::cout << "lambda called for: " << i << '\n';
  return 0;


  • foreach (primes.begin(), primes.end(), func);When passed by value, the transfer function is decay as a function pointer.
  • foreach (primes.begin(), primes.end(), &func); This is more direct. It directly passes a function pointer.
  • foreach (primes.begin(), primes.end(), FuncObj());This is what I said abovefunctor, an overloadoperator()Class. So, when calledop(*current);Is actually calledop.operator()(*current);. PS. if const after the function declaration is not added, an error may be reported in some compilers.
  • Lambda: This is the same as the previous situation. I won’t explain it.

Handle member functions and additional parameters

There is no scenario mentioned above: member functions. Because the way to call a non static member function isobject.memfunc(. . . )orptr->memfunc(. . . ), not uniformfunction-object(. . . )


Fortunately, since C + + 17, C + + has providedstd::invoke<>()To unify all callback forms:


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

So,std::invoke<>()How to unify all forms of callback?
Notice that we added a third parameter to foreach:Args const &... args. invoke is handled as follows:

  • If callable is a pointer to a member function,It uses the first argument of args as this of the class. The remaining parameters in args are passed to callable.
  • Otherwise, all args are passed to callable.


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

int main() {
  std::vector 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 << '\n';
      "- 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

Notice how foreach is called when callback is a member function.

Unified packaging

std::invoke()A scenario usage of is to wrap a function call, which can be used to record the function call log, measure the time, etc.

#include                // for std::invoke()
#include         // for std::forward()

decltype(auto) call(Callable&& op, Args&&... args) {
    return std::invoke(std::forward(op),  std::forward(args)...);       // passed callable with any additional args

One thing to consider is how to handle the return value of OP and return it to the caller:

decltype(auto) call(Callable&& op, Args&&... args)

Use heredecltype(auto)(from C + + 14)(decltype(auto)See the previous article: core knowledge of C + + 11-17 template (IX) — understanding decltype and decltype (auto))

If you want to process the return value, you can declare the return value asdecltype(auto)

decltype(auto) ret{std::invoke(std::forward(op), std::forward(args)...)};

return ret;

But there is a problem, usingdecltype(auto)Declare a variable. The value cannot be void. You can process void and non void respectively:

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

decltype(auto) call(Callable &&op, Args &&... args) {

  if constexpr (std::is_same_v, void>) {
    // return type is void:
    std::invoke(std::forward(op), std::forward(args)...);
  } else {
    // return type is not void:
    decltype(auto) ret{
        std::invoke(std::forward(op), std::forward(args)...)};
    return ret;

std::invoke_result<>It can only be used from C + + 17. It can only be used before C + + 17typename std::result_of::type.

Other basic techniques of generic Libraries

Type Traits

Many people should be familiar with this technology. I won’t elaborate here.


class C {

  // ensure that T is not void (ignoring const or volatile):
  static_assert(!std::is_same_v, void>,
                "invalid instantiation of class C for void type");

  template  void f(V &&v) {
    if constexpr (std::is_reference_v) {
      ... // special code if T is a reference type
    if constexpr (std::is_convertible_v, T>) {
      ... // special code if V is convertible to T
    if constexpr (std::has_virtual_destructor_v) {
      ... // special code if V has virtual destructor

Here, we use type_ Traits for different implementations.


have access tostd::addressof<>()Get object or functionReal address, even if it is overloadedoperator &However, this situation is not very common. When you want to get any type of real address, it is recommended to usestd::addressof<>():

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

For example, in STL Vector, when the vector needs to be expanded, migrate the code of the old and new vector elements:

  for (; __first != __last; ++__first, (void)++__cur) std::_Construct(std::__addressof(*__cur), *__first);
  return __cur;

inline void _Construct(_T1 *__p, _Args &&... __args) {
  ::new (static_cast(__p)) _ T1(std::forward(__args)...);      // Actual copy (or move) element

Use herestd::addressof()Get the address of the current element of the new vector, and then copy (or move). You can see the previously written C + + application scenario of noexcept from the expansion of vector


std::declvalCan be considered a placeholder for a specific type of object reference. It does not create objects and is often used with decltype and sizeof. Therefore, without creating an object, you can assume that there is an available object of the corresponding type, even if the type does not have a default constructor or the type cannot create an object.

Note that declval can only be used in unevaluated contexts.

A simple example:

class Foo;     //forward declaration
Foo f(int);     //ok. Foo is still incomplete
using f_result = decltype(f(11));      //f_result is Foo

Now, if I want to get what type is returned after calling f () with int? yesdecltype(f(11))? It looks strange. Using declval looks very clear:


There is also the core knowledge of the previous C + + 11-17 template (I) — an example in the function template) — return the common types of multiple template parameters:

template ()
                                                   : std::declval())>>
RT max(T1 a, T2 b) {
  return b < a ? a : b;

Here to avoid?:We have to call the constructors of T1 and T2 to create objects. We use declval to avoid creating objects, and we can achieve our goal. PS. don’t forget to use STD:: decay_ t. Because declval returns an rvalue references. If not,max(1,2)Will returnint&&.

Finally, take a look at the example on the official website:

struct Default { int foo() const { return 1; } };
struct NonDefault
    NonDefault() = delete;
    int foo() const { return 1; }
int main()
    decltype(Default().foo()) n1 = 1;                   // type of n1 is int
//  decltype(NonDefault().foo()) n2 = n1;               // error: no default constructor
    decltype(std::declval().foo()) n2 = n1;    // type of n2 is int
    std::cout << "n1 = " << n1 << '\n'
              << "n2 = " << n2 << '\n';

Perfect forwarding

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

Or forward temporary variables to avoid irrelevant copy overhead:

void foo(T x) {
    auto&& val = get(x);

    // perfectly forward the return value of get() to set():

Reference as template parameter

void tmplParamIsReference(T) {
    std::cout << "T is reference: " << std::is_reference_v << '\n';

int main() {
    std::cout << std::boolalpha;
    int i;
    int& r = i;
    tmplParamIsReference(i);     // false
    tmplParamIsReference(r);      // false
    tmplParamIsReference(i);      // true
    tmplParamIsReference(r);      // true

This is not very common. In the previous article C + + 11-17 template core knowledge (VII) – template parameters are passed by value vs by reference. This will change the behavior of the template, even if the template designer doesn’t want to design it at first.

I haven’t seen much of this usage, and it sometimes has pits. Just learn about it.

You can use static_ Assert prohibits this use:

class optional {
    static_assert(!std::is_reference::value, "Invalid instantiation of optional for references");

Defer evaluations

First, introduce a concept: incomplete types. The type can be complete or incomplete. Incomplete types include:

  • Class only declares undefined.
  • Array has no size defined.
  • The array contains incomplete types.
  • void
  • The underlying type of the enumeration type or the value of the enumeration type is not defined.

It can be understood that incomplete types only defines an identifier, but does not define a size. For example:

class C;     // C is an incomplete type
C const* cp;     // cp is a pointer to an incomplete type
extern C elems[10];     // elems has an incomplete type
extern int arr[];     // arr has an incomplete type
class C { };     // C now is a complete type (and therefore cpand elems no longer refer to an incomplete type)
int arr[10];     // arr now has a complete type

Now return to the topic of defer evaluations. Consider the following types of templates:

class Cont {
    T* elems;

Now this class can use incomplete type, which is very important in some scenarios, such as the simple implementation of linked list nodes:

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

However, once some types are used_ Traits, the class no longer accepts incomplete type:

class Cont {
  T *elems;

  typename std::conditional::value, T &&, T &>::type 

std::conditionalIt’s also a type_ Traits, which means that foo () returns according to whether T supports mobile semanticsT &&stillT &.

But the problem is,std::is_move_constructibleThe required parameter is a complete type. Therefore, the previous declaration of struct node will fail (not all compilers will fail. In fact, I understand that an error should not be reported here, because according to the rules of class template instantiation, member functions are instantiated only when they are used).

We can use defer evaluations to solve this problem:

class Cont {
  T *elems;

  typename std::conditional::value, T &&, T &>::type 

In this way, the compiler will not instantiate foo () until it is called by the node of complete type.


Friends can take note of my official account and get the most timely updates.

Recommended Today

Notes on basic learning of ruby metaprogramming

Note 1:The code contains variables, classes and methods, which are collectively referred to as language construct. ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # test.rb class Greeting  def initialize(text)   @text = text  end    def welcome   @text  end end my_obj =“hello”) […]