Learning records of C++ Primer on October 12, 2021: Chapter 16

Chapter 16 templates and generic programming

16.1 definition template

A template is a blueprint or formula for creating classes or functions.

16.1.1 function template

Define a function template compare:

template <typename T>
int compare(const T &v1, const T &v2) {
    if (v1 < v2) return -1;
    else if (v2 < v1) return 1;
    return 0;

(1) Instantiation function template

Compilers usually use function arguments to infer template arguments for us.

(2) Template type parameters

The keyword class or typename must be used before the type parameter:

// Error, class or typename must be added before U
template <typename T, U> T calc(const T&, const U&);

Using the typename keyword is more intuitive.

(3) Non type template parameters

In addition to defining type parameters, we can also define non type parameters in the template, but their values must be constant expressions:

template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]) {
    return strcmp(p1, p2);

(4) Function templates for inline and constexpr

The inline or constexpr specifier is placed after the template parameter list and before the return type:

template <typename T>
inline T min(const T&, const T&);

(5) Template compilation

When the compiler encounters a template definition, it does not generate code. The compiler generates code only when we use a template.

The definitions of function templates and class template member functions are usually placed in header files.

(6) Most compilation errors are reported during instantiation

The template will not generate code until instantiation, which affects when we will know the compilation errors of the code in the template. Typically, the compiler reports errors in three stages:

  1. The first stage: when compiling the template.

    At this stage, the compiler usually does not find many errors. The compiler can check for syntax errors, such as forgetting semicolons or misspelling variable names, but that's all.

  2. The second stage: when the compiler encounters template use.

    At this stage, the compiler still doesn't have much to check. For function template calls, the compiler usually checks whether the number of arguments is correct. It can also check whether the parameter types match. For class templates, the compiler can check whether the user has provided the correct number of template arguments, but it is limited to this.

  3. The third stage: template instantiation.

    Only at this stage can type related errors be found. Depending on how the compiler manages instantiation, such errors may be reported at link time.

16.1.2 type formwork

(1) Define class template

template <typename T> class Blob {
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;
    // Constructor
    Blob(std::initializer_list<T> il);
    // Number of elements in Blob
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    // Add and remove elements
    void push back(const T &t) { data->push_back(t); }
    // Mobile version
    void push back(T &&t){ data->push_back(std::move(t)); }
    void pop back();
    // Element access
    T& back();
    T& operator[](size_type i); 
    std::shared_ptr<std::vector<T>> data;
    // If data[i] is invalid, msg is thrown
    void check(size_type i, const std::string &msg) const;

(2) Instantiate class template

When using a class template, we must provide additional information. This additional information is an explicit list of template arguments that are bound to template parameters. The compiler uses these template arguments to instantiate specific classes.

Blob<int> ia;
Blob<int> ia2 = { 0, 1, 2, 3, 4 };

ia and ia2 use the same specific type version of Blob (i.e. Blob < int >). From these two definitions, the compiler instantiates a class equivalent to the following definitions:

template <> class Blob<int> {
    typedef typename std::vector<int>::size_type size_type;
    Blob(std::initializerlist<int> il);
    int& operator[] (size_type i);
    std::shared_ptr<std::vector<int>> data;
    void check(size_type i, const std::string &msg)const;

When the compiler instantiates a class from our Blob template, it will rewrite the Blob template and replace each instance of the template parameter T with the given template argument, which is int in this case.

(3) Member functions of class templates

We can define member functions inside or outside the class template, and the member functions defined in the class template are implicitly declared as inline functions.

Member functions defined outside the class template must start with the keyword template, followed by the class template parameter list.

(4) Simplify the use of template class names within class code

When we use a class template type, we must provide template arguments.

There is one exception to this rule. In the scope of the class template itself, we can directly use the template name without providing arguments.

// BlobPtr throws an exception if it attempts to access an element that does not exist
template <typename T> class BlobPtr {
    BlobPtr() : curr(0) { }
    BlobPtr(Blob<T> &a, size_t sz = 0) : wptr(a.data), curr(sz) { }
    T& operator* () const { 
        auto p = check(curr, "dereference past end");
        return(*p)[curr];      // (* p) is the vector pointed to by this object
    // Increasing and decreasing
    BlobPtr& operator++();     //Prepositive operator
    BlobPtr& operator--();
    // If the check is successful, check returns a shared to the vector_ ptr 
    std::shared_ptr<std::vector<T>> check(std::size t, const std::string&) const;
    // Save a break_ PTR, indicating that the underlying vector may be destroyed
    std::weak_ptr<std::vector<T>> wptr;
    std::size_t curr;          // Current position in array

The leading increment and decrement members of BlobPtr return BlobPtr &, instead of BlobPtr < T > &. When we are in the scope of a class template, the compiler processes the reference of the template itself as if we had provided arguments matching the template parameters.

(5) Use the class template name outside the class template

When we define its members outside the class template, we are not in the scope of the class. We do not enter the scope of the class until we encounter the class name:

// Post: increment / decrement the object but return the original value 
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int) {
    // There is no need to check here; This is checked when calling pre increment
    BlobPtr ret = *this;     // Save current value
    ++*this;                 // Advance an element; Pre + + check whether increment is legal
    return ret;              // Return to saved state

In the function body, we have entered the scope of the class, so there is no need to repeat the template arguments when defining ret. Therefore, the definition of RET is equivalent to the following code;

BlobPtr<T> ret = *this;

(6) Class templates and friends

If a class template contains a non template friend, the friend is authorized to access all template instances. If the friend itself is a template, the class can be authorized to all friend template instances or only to specific instances.

  1. One to one friendly relationship

    // Pre declaration, which is required to declare friends in Blob
    template <typename> class BlobPtr;
    template <typename> class Blob;       //Operator = = required for parameter in
    template <typename T> bool operator==(const Blob<T>&, const Blob<T>&);
    template <typename T> class Blob {
        // Each Blob instance grants access to BlobPtr and equality operators instantiated with the same type
        friend class BlobPtr<T>;
        friend bool operator==<T>(const Blob<T>&, const Blob<T>&);

    Friends' declarations use Blob's template parameters as their own template arguments. Therefore, the friendly relationship is limited between Blob and BlobPtr equality operators instantiated with the same type.

  2. Generic and specific template friendliness

    A class can also declare each instance of another template as its own friend, or define a specific instance as a friend:

    // Pre declaration, which is used when declaring a specific instance of the template as a friend
    template <typename T> class Pal;
    class C {                          // C is an ordinary non template class
        friend class Pal<C>;           // Pal instantiated with Class C is a friend of C
        // All instances of Pal2 are friends of C; In this case, no pre declaration is required
        template <typename T> friend class Pal2;
    template <typename T> class C2 {   // C2 itself is a class template
        // Each instance of C2 declares the same instantiated Pal as a friend
        friend class Pal<T>;           // Pal's template declaration must be within scope
        // All instances of Pal2 are friends of each instance of C2, and no pre declaration is required
        template <typename X> friend class Pal2;
        // Pal3 is a non template class, which is a friend of all instances of C2
        friend class Pal3;              //Pre declaration of Pal3 is not required

    In order for all instances to become friends, the friend declaration must use template parameters different from the class template itself.

  3. Make the template's own type parameter a friend

    In the new standard, we can declare template type parameters as friends:

    template <typename Type> class Bar {
        friend Type;        // Grant access to the type used to instantiate the Bar

    Here, we declare the type used to instantiate the Bar as a friend. Therefore, for a certain type name Foo, Foo will become a friend of Bar < Foo >, Sales_data will become Bar < Sales_data > and so on.

(7) Template type alias

typedef Blob<string> StrBlob;

// New standard mode of C++11
template <typename T> using twin = pair<T, T>;
twin<double> area;    // area is a pair < double, double >

(8) static member of class template

template <typename T> class Foo {
    static std::size_t count() { return ctr; }
    // Other interface members
    static std::size_t ctr;
    // Other implementation members

template <typename T>
size_t Foo<T>::ctr = 0;    // Initialize ctr

Like any other member function, a static member function is instantiated only when used.

16.1.3 template parameters

(1) Template declaration

The name of the template parameter in the declaration does not have to be the same as that in the definition as the function parameter:

// All three calc s point to the same function template
template <typename T> T calc(const T&,const T&);  // statement
template <typename U> U calc(const U&,const U6);  // statement

// Definition of template
template <typename Type>
Type calc(const Type& a, const Type& b) { /* ...*/ }

(2) Use type members of classes

Assuming T is the name of a type parameter, when the compiler encounters a statement of the following form:

​ T::size_type *pi

It needs to know whether we are defining a variable named p or a variable named size_ The static data member of type is multiplied by a variable named p.

By default, the C + + language assumes that the name accessed through the scope operator is not a type. Therefore, if we want to use a type member of a template type parameter, we must explicitly tell the compiler that the name is a type. We do this by using the keyword typename:

template <typename T>
typename T::value_type top(const T& c) {
        return c.back();
        return typename T::value_type();

(3) Default template arguments

We can also provide default template arguments. In the new standard, we can provide default arguments for function and class templates. Earlier C + + standards allowed only default arguments for class templates.

For example, we override compare and use the less function object template of the standard library by default:

// compare has a default template argument less < T > and a default function argument F()
template <typename T, typename F = less<T>>
int compare(const T &vl, const T &v2, F f = F()) {
    if (f(vl, v2)) return -1;
    if (f(v2, v1)) return 1;
    return 0;

(4) Template default real participation class template

Whenever we use a class template, we must put angle brackets after the template name. Angle brackets indicate that the class must be instantiated from a template.

In particular, if a class template provides default arguments for all its template parameters, and we want to use these default arguments, we must follow the template name with an empty angle bracket pair:

template <class T = int> class Numbers {    // T defaults to int 
    Numbers(T v = 0): val(V) { }
    // Various operations on values
    T val;

Numbers<long double> lots_of_precision;
Numbers<> average_precision;                // An empty < > indicates that we want to use the default type
16.1.4 member template

Member templates cannot be virtual functions.

(1) Member templates for normal (non template) classes

As with any function template, the type of T is inferred by the compiler.

(2) Member template of class template

Unlike ordinary function members of class templates, member templates are function templates. When we define a member template outside the class template, we must provide a template parameter list for both the class template and the member template. The parameter list of the class template is followed by the member's own template parameter list:

template <typename T>     // Type parameters of class
template <typename It>    // Type parameter of constructor
Blob<T>::Blob(It b, It e) : data(std::make_shared<std::vector<T>>(b, e)) { }

(3) Instantiation and member templates

Like ordinary function templates, the compiler usually infers its template arguments from the function arguments passed to the member template.

16.1.5 control instantiation

When two or more independently compiled source files use the same template and provide the same template parameters, there will be an instance of the template in each file.

In large systems, the overhead of instantiating the same template in multiple files can be very serious. In the new standard, we can avoid this overhead by explicit instantiation. An explicit instantiation is as follows:

extern template class Blob<string>;     // Instantiation declaration
template class Blob<string>;            // Instantiation definition

When the compiler encounters an extern template declaration, it will not generate instantiation code in this file. Declaring an instantiation as extern means that it promises to have a non extern declaration (definition) of the instantiation elsewhere in the program. For a given instantiated version, there may be multiple extern declarations, but there must be only one definition.

Because the compiler instantiates a template automatically when it is used, the extern declaration must appear before any code that uses this instantiated version.

The instantiation definition of a class template instantiates all members of the template, including inline member functions.

16.2 template argument inference

16.2.1 type conversion and template type parameters

There are two types of type conversions that can be applied to template functions:

  1. const conversion

    You can pass a reference (pointer) of a non const object to a const reference (pointer) parameter

  2. Array or function pointer conversion

    If the function parameter is not a reference type, normal pointer conversion can be applied to the array or function type argument:

    • An array argument can be converted to a pointer to its first element
    • A function argument can be converted to a pointer of the function type

Other type conversions (such as arithmetic conversion, conversion from derived class to base class, etc.) cannot be applied to function templates.

(1) Function parameters using the same template parameter type

The compare function accepts two const t& parameters, which must be of the same type:

long lng;
compare(lng, 1024);     // Error, unable to instantiate compare(long, int)

If you want to perform normal type conversion on function arguments, you can define the function template as two type parameters:

template <typename A, typename B>
int flexibleCompare(const A& v1, const B& v2) {
    if (v1 < v2) return -1;
    else if (v2 < v1) return 1;
    return 0;

(2) Normal type conversions apply to normal function arguments

Function templates can have parameters defined by common types. Such parameters that do not involve template types can be normally converted to the types of corresponding formal parameters:

template <typename T>
ostream &print(ostream &os, const T &obj) {
    return os << obj;

print(cout, 42);         // Instantiate print (ostream&, int)
ofstream f("output");    
print(f, 10);            // Instantiate print (ostream &, int) and convert f to ostream&
16.2.2 function template display arguments

(1) Specify explicit template arguments

We define the third template parameter representing the return type, which allows the user to control the return type:

template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);

Since there is no parameter type to infer T1, an explicit template argument must be provided for T1 every time sum is called:

auto val3 = sum<long long>(i, lng);    // long long sum(int, long)

(2) Normal type conversions are applied to display the specified arguments

For template type parameters, the specified function arguments have been displayed, and normal type conversion is also performed:

long lng;
compare(lng, 1024);         // error
compare<long>(lng, 1024);   // compare(long, long)
compare<int>(lng, 1024);    // Correct, compare(int, int)
16.2.3 tail return type and type conversion

For cases where the return type of the template function is unclear, we can use the trailing return type, allowing us to declare the return type after the parameter list:

template <typename It>
auto fcn(It beg, It end) -> decltype(*beg) {
    // handle
    return *beg;    // Return reference

(1) Standard library template class for type conversion

If we want the above function to return not a reference but a value, we can use the type conversion templates of the standard library, which are defined in the header file < type_ Traits > in:
M o d T M o d < T > : : t y p e remove_reference X&   or   X&& X otherwise T add_const X&,const   X   Or function T otherwise const T add_lvalue_reference X& T X&& X& otherwise T& add_rvalue_reference X&   or   X&& T otherwise T&& remove_pointer X* X otherwise T add_pointer X&   or   X&& X* otherwise T make_signed unsigned X X otherwise T make_unsigned Signed type unsigned X otherwise T remove_extent X[n] X otherwise T remove_all_extents X[n1][n2] ... X otherwise T \At the beginning of the{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{& \ text {x \ &} & \ text {t} \ \ & \ text {x \ & \ &} &The text{isthe first of the \ \ \ {\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ text{texttext \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ & \ text text text{{{{{text text text text text{{{{{{{{{{{{{\ \} \ \} \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ text text text text{{{x {{{{{{\ \ \ \ \ \ hlitext text text text text text text{\ \ \ \ \ \ hlitext text text text text text text text text text text text text text text text text text text text text text text text text text text text text text \ \ \ \ \ \ \ \ \ \ hlitext text text text text text text text text text text text text text x *} \ \ & \ text {otherwise} & \ text {t} \ \ \ hline \ text One of the first places to make a great deal is the & & \ \ \ text{{{'' '' '' '' '' '' {{{{{{\ \ \ \ text text \ \ \ \ \ \ {{{{{{{{{{{{{{{{{{{{{{{{{{{} \ \ \ text text {{{{text {{\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ text text text{\ text text {\ text {{\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ text text text {{{text text {text {text {text {text {text {text \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ text {x [N1] [N2]} \ dots & \ text {x} \ \ & \ text {otherwise} & \ text {t} \ \ \ hline \ end {array} Modremove_referenceadd_constadd_lvalue_referenceadd_rvalue_referenceremove_pointeradd_pointermake_signedmake_unsignedremove_extentremove_all_extents​TX&   or   X & & otherwise X &, const   X   Or function otherwise X & X & & otherwise x&   or   X & & otherwise x * otherwise x&   or   X & & otherwise unsigned   X otherwise signed type otherwise X[n] otherwise X[n1][n2]... Otherwise mod < T >:: typexttconst   TTX&T&TT&&XTX*TXTunsigned   XTXTXT​​
Code implementation:

template <typename It>
auto fcn(It beg, It end) -> 
typename remove_reference<decltype(*beg)>::type {
    // handle
    return *beg;    // Return reference
16.2.4 template argument inference and reference

(1) Inferring types from lvalue reference function parameters

  1. T&

    template <typename T> void f1(T&);  // Argument must be an lvalue
    f1(i);    // i is an int, and the template parameter type T is int
    f1(ci);   // ci is a const int, and the template parameter type T is const int
    f1(5);    // error
  2. const T&

    template <typename T> void f2(const T&);  // An R-value is acceptable
    f2(i);    // i is an int, and the template parameter type T is int
    f2(ci);   // ci is a const int, and the template parameter type T is int
    f2(5);    // A const & parameter can be bound to an R-value, and T is int

(2) Inferring types from R-value reference function parameters

template <typename T> void f3(T&&);

f3(42);    // The argument is an R-value of type int, and the template parameter T is int

(3) Reference collapse and R-value reference parameters

Generally, we cannot bind an R-value reference to an l-value. For example, calls such as f3(i) are considered illegal. However, C + + allows two exceptions:

  1. When an lvalue (i) is passed to the template type R-value reference parameter (T & &) of the function, the compiler infers that the template type parameter (T) is the lvalue reference type of the argument
  2. If a referenced reference (such as the first rule) is created indirectly, these applications will be collapsed:
    • X & &, X & &, X & & & are all folded into X&
    • X & & & & fold to X&&

These two exceptions lead to the following two important results:

  1. If a function parameter is an R-value reference to a template type parameter (e.g., T & &), it can be
    Bound to an lvalue; and
  2. If the argument is an lvalue, the inferred template argument type will be an lvalue reference and the function parameter will be
    Instantiated as a (normal) lvalue reference parameter (T &)
template <typename T> void f3(T&&);

f(i);   // T is inferred as int&
f(ci);  // T is inferred as const int&

(4) Write a template function that accepts an R-value reference parameter

The above template function codes that accept right value references may have unexpected results for different argument types:

template <typename T>
void f3(T&& val) {
    T t = val;        // Is t a reference type or a value type? Is t a reference or a copy?
    t = fcn(t);       // Do you change val at the same time or just t?
    if (val == t) { /* ... */ }    // If t is a reference, the judgment result is always true

Therefore, in practice, the function template referenced by the right value is usually overloaded in the following ways:

template <typename T> void f(T&&);         // Bind to non const right value
template <typename T> void f(const T&);    // Lvalue and const lvalue
16.2.5 understanding std::move

move definition of standard library:

template <typename T>
typename remove_reference<T>::type&& move(T&& t) {
    return static_cast<typename remove_reference<T>::type&&> (t);

move makes use of two special cases of C + + introduced in the previous section, where

  • The parameter T & & T allows move to accept all types of arguments
  • Remove_reference < T > returns the corresponding value type regardless of whether T is a reference or a value type
  • Return type remove_reference < T >:: Type & & makes move always return an R-value reference
16.2.6 forwarding

Some functions need to forward one or more arguments to other functions with the same type. In this case, we need to maintain all the properties of the forwarded arguments, including whether the argument type is const and whether the argument is lvalue or lvalue.

For example, we write a function that accepts a callable expression and two additional arguments, and pass the two arguments to the expression in reverse order:

// A template that accepts a callable object and two other parameters
// Call the given callable object on the flip parameter
// flip1 is an incomplete implementation: the top-level const and reference are missing
template <typename F, typename T1, typename T2>
void flip1(F f, T1 tl, T2 t2) {
    f(t2, t1);

When the argument we pass is a reference, we can find that f(t2, t1) may pass a value, so it does not achieve the original expected effect.

(1) Use right valued parameter optimization

According to the previous introduction, we can change t into T & &:

template <typename F, typename T1, typename T2>
void flip2(F f, T1&& tl, T2&& t2) {
    f(t2, t1);

This optimized code can well maintain the const, lvalue and rvalue properties of the flip argument.

However, this version can only solve half the problem, but it can't deal with functions that accept R-value reference parameters.

Suppose the expression g we passed in is defined as follows:

void g(int &&i, int &j) {
    cout << i << " " << j << endl;

flip2(g, i, 42);    // Error, cannot instantiate int with lvalue&&

Because the argument passed in is (g, i, 42), the type of t2 binding is int, but in the flip2 function, t2 (lvalue) is passed to the formal parameter i (lvalue) of G, which is obviously not allowed.

(2) Use std::forward to keep type information

We can use a new standard library facility named forward to pass the parameter of flip2, which can maintain the type of the original argument. It is defined in the header file utility. Forward must be called through an explicit template argument. Forward returns the right value reference of the explicit argument type. That is, the return type of forward < T > is T & &.

Normally, we use forward to pass the function parameters defined as the R-value reference of the template type parameter. By folding the reference on its return type, forward can maintain the l-value / R-value attribute of the given argument:

template <typename Type> intermediary (Type &&arg) {
    finalFcn (std::forward<Type>(arg));

For our example, the modified code is as follows:

template <typename F, typename T1, typename T2>
void flip1(F f, T1&& tl, T2&& t2) {
    f(std::forward<T2> (t2), std::forward<T2> (t1);

16.3 heavy load and formwork

The function matching rules involving the function template are as follows:

  • For a call, the candidate function includes all template arguments, and the function template instance that is successfully inferred.
  • Candidate function templates are always feasible because template argument inference excludes any infeasible templates.
  • As usual, viable functions (template and non template) are sorted by type conversion if required for this call. Of course, the type conversions that can be used for function template calls are very limited.
  • As always, if there is just one function that provides a better match than any other function, select this function. However, if multiple functions provide the same good match, then:
    • If only one of the equally good functions is a non template function, select this function.
    • If there is no non template function in the same good function, but there are multiple function templates, and one of the templates is better than
      If other templates are more specific, select this template.
    • Otherwise, this call is ambiguous.

(1) Writing overloaded templates

As a set of examples, we write a set of debugging functions debug_rep, each function returns a string representation of a given object:

// Print any type we can't handle
template <typename T> string debug_rep(const T &t) {
    ostringstream ret;
    ret << t;             // Print a representation of T using the output operator of T
    return ret.str();     // Returns a copy of the string bound by ret

// Prints the value of the pointer, followed by the object pointed to by the pointer
// Note: this function cannot be used for char*
template <typename T> string debug_rep(T *p) {
    ostringstream ret;
    ret << "pointer: " << p;         // Print the value of the pointer itself
    if (p)
        ret << " " << debug_rep(*p); // Print the value pointed to by p
        ret<< " null pointer";       // Or indicate that p is empty
    return ret.str();                // Returns a copy of the string bound by ret

Calls to some functions:

string s("hi");

cout << debug_rep(s) << endl;    // Call debug_ rep(const T &t)
cout << debug_rep(&s) << endl;   // Call debug_rep(T *p)

(2) Multiple feasible templates

Consider a special example:

const string *sp = &s;
cout << debug_rep(sp) << endl;   // Call debug_rep(T *p)

Both templates in this example are feasible, and both are exactly matched:

  • debug_rep(const string *&)

    By the first version of debug_rep is instantiated, and T is bound to string *.

  • debug_rep(const string*)

    By the second version of debug_rep is instantiated, and T is bound to const string.

In this case, the normal function matching rule cannot distinguish between the two functions. We may feel that this call will be ambiguous. However, according to the special rules of overloaded function templates, this call is resolved to debug_rep (T *), that is, a more specialized version.

The reason why this rule is designed is that without it, the pointer version of debug cannot be called on a const pointer_ rep. The problem is template debug_rep (const T &) can be used for virtually any type, including pointer types. This template is better than debug_rep(T *) is more general, which can only be used for pointer types. Without this rule, calls that pass const pointers are always ambiguous.

(3) Non template and template overloading

As the next example, we define a normal non template version of debug_rep to print double quoted string:

// Print a string enclosed in double quotes
string debug_rep(const string &s) {
    return '"' + s + '"';

Now call debug on a string_ rep:

string s("hi");
cout << debug_rep(s) << endl;  // Call debug_ rep(const string &s)

There are two equally good feasible functions:

  • debug_rep<string>(const string&)

    The first template, T, is bound to string *.

  • debug_rep(const string&)

    Ordinary non template functions.

However, the compiler will choose the non template version because it is more special, and a non template function is better than a function template.

(4) Overloading templates and type conversions

Consider the matching of C-style strings:

cout << debug_rep("hi world!") << endl;  // Call debug_rep(T*)

In fact, for three versions of debug_rep is feasible:

  • debug_rep(const T&)

    T is bound to char[10].

  • debug_rep (T*)

    T is bound to const char.

  • debug_rep(const string&)

    Type conversion from const char * to string is required.

Similarly, the T * version is more specialized, so the compiler will choose it.

16.4 variable parameter template

A variable parameter template is a template function or template class that accepts a variable number of parameters.

  • **Parameter package: * * variable parameters
    • **Template parameter package: * * indicates 0 or more template parameters
    • **Function parameter package: * * indicates 0 or more function parameters
// Args is a template parameter package; rest is a function parameter package
// Args represents zero or more template type parameters
// rest represents zero or more function parameters
template <typename T, typename... Args>
void foo(const T &t, const Args& ...rest);

The compiler infers the type of template parameters from the arguments of the function, and also infers the number of parameters in the package:

int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d);    // There are three parameters in the package
foo(s, 42, "hi");    // There are two parameters in the package
foo(d, s);           // There is a parameter in the package
foo("hi");           // Empty packet

The compiler instantiates the following versions:

void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);

When we need to get to the number of elements in the package, we can use the sizeof... Operator to return a constant expression:

template <typename ... Args> void f(Args... args) {
    cout << sizeof...(Args) << endl;
    cout << sizeof...(args) << endl;
16.4.1 preparation of variable parameter function template
// A function that terminates recursion and prints the last element
// This function must be declared before the print definition of the variable parameter version
template<typename T>
ostream &print(ostream &os, const T &t) {
    return os << t;    // No separator is printed after the last element in the package

// All elements in the package except the last element will call this version of print 
template <typename T, typename...Args>
ostream &print(ostream &os, const T &t, const Args&...rest) {
    // Print first argument
    os << t<< ",";
    // Recursive call to print other arguments
    return print(os, rest...);
16.4.2 package extension

For a parameter package, in addition to obtaining its size, the only thing we can do for it is to expand it.

When extending a package, we also provide a schema for each extension element. Extending a package is to decompose it into constituent elements, apply patterns to each element, and obtain the expanded list. We trigger the extension operation by placing an ellipsis (...) to the right of the pattern.

// Debug is called for each argument in the print call_ rep 
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&...rest) {
    // print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an))
    return print(os, debug_rep(rest)...);

This print call uses the pattern debug_reg(rest). This pattern indicates that we want to call debug for each element in the function parameter package rest_ rep. The extension result will be a comma separated debug_rep call list:

errorMsg(cerr, fcnName, code.num(), otherData, "other", item);

It's like we write code like this

print(cerr, debug_rep(fcnName), debug_rep(code.num(),
            debug_rep(otherData), debug_rep("otherData"), 

In contrast, the following modes fail to compile:

// Pass package to debug_rep; print(os, debug_rep(a1,a2,...,an))
print(os, debug_rep(rest...));   // Error: no matching function for this call

The problem with this code is that we are debugging_ rest is extended in rep call, which is equivalent to

print(cerr, debug_rep(fcnName, code.num(), otherData, "otherData", item));

16.5 formwork specialization

In some cases, the definition of a generic template is not appropriate for a particular type. At other times, we can define a specialized version of a class or function template. Take the compare function as an example:

// The first version; Any two types can be compared
template <typename T> int compare(const T&, const T&);

// The second version handles string literal constants
template <size_t N,size t M>
int compare(const char (&)[N], const char (&)[M]);

However, if you pass in two pointers to string constants, the compiler will still call the first version:

const char *p1 = "hi", *p2 = "mom";

compare(p1, p2);         // Call the first template
compare("hi", "mom");    // Call the second template

Therefore, we also need to make a special case for this situation

(1) Define function template specialization

When making a template special, you need to provide all the arguments, and the template followed by < > means that we will provide all the arguments:

// A special version of compare that handles pointers to character arrays
template <>
int compare(const char* const &p1, const char* const *p2) {
    return strcmp(p1, p2);

Here, in order to correspond to the specialization of the previous template template < typename T > int compare (const t&, const t&), we need a reference to const char *. In fact, t corresponds to const char *, so the formal parameter type we define should be const char * const &, where the second const corresponds to const in const T &.

(2) Function overloading and template specialization

When defining a specialized version of a function template, we essentially take over the work of the compiler. That is, we provide definitions for a special instance of the original template. It is important to understand that a specialized version is essentially an instance, not an overloaded version of a function name.

The essence of specialization is to instantiate a template, not overload it. Therefore, specialization does not affect function matching.

Tags: C++

Posted on Sun, 24 Oct 2021 15:02:35 -0400 by Helaman