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

Time:2021-9-13
  • 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

Callables

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:

template 
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 {
public:
  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;
}

Explain:

  • 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(. . . )

std::invoke<>()

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

image

template 
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;
  }
}

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.

use:

// 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 << '\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()

template
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:

template
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()

template 
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)...);
    ... 
    return;
  } 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.

#include 

template  
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");

public:
  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.

std::addressof()

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<>():

template
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;
}

template 
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::declval

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:

decltype(f(std::declval()))

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:

#include 
#include 
 
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

template
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:

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

    // perfectly forward the return value of get() to set():
    set(std::forward(val));
}

Reference as template parameter

template
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:

template
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:

template
class Cont {
  private:
    T* elems;
  public:
    ...
};

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:

template  
class Cont {
private:
  T *elems;

public:
  ... 
  
  typename std::conditional::value, T &&, T &>::type 
  foo();
};

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:

template  
class Cont {
private:
  T *elems;

public:
  ... 
  
  template
  typename std::conditional::value, T &&, T &>::type 
  foo();
};

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

(end)

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 = Greeting.new(“hello”) […]