Chapter 16 Templates and Generic Programming
Define Template
Function Template
template<typename T> int compare(const T &v1, const T &v2) { if(v1 < v2) return -1; if(v2 < v1) return 1; return 0; }
- Template definitions start with the keyword template followed by a list of template parameters, which is a comma-separated list of one or more template parameters surrounded by <>
- Template parameter lists act much like function parameter lists, which define several specific types of local variables but do not indicate how to initialize them. Caller provides arguments to initialize parameters at runtime
- Similarly, template parameters use types or values in class or function definitions. When using a template, we specify the template argument and bind it to the template parameter
- When we call a function template, the compiler usually uses function arguments to infer template arguments for us. The compiler instantiates a specific version of the function for us with inferred template parameters. When the compiler instantiates a template, it creates an instance of the template using the actual template arguments instead of the corresponding template parameters
// Instantiate int compare (const int&, const int&) cout << compare(1,0) <<endl; // Instantiate int compare (const vector <int>&, const vector <int>&) vector<int> vec1{1,2,3}, vec2{4,5,6}; cout << compare(vec1, vec2) << endl;
- Versions generated by these compilers are called instances of templates
- Our compare function has a template type parameter. Generally speaking, we can think of a type parameter as a type specifier, just like a built-in type or class type specifier. In particular, a type parameter can be used to specify the parameter type of a return type or function, as well as a variable declaration or type conversion within a function.
// Correct: The return type and parameter type are the same template <typename T> T foo(T* p) { T tmp = *p; // ... return tmp; }
- Type parameters must be preceded by the keyword class or typename, which means the same in the template parameter list and can be used interchangeably. You can use two keywords in a template parameter list at the same time. But the typename keyword is more intuitive for specifying the template type
- In addition to defining type parameters, you can also define untyped parameters in templates, which represent a value rather than a type. We specify untyped parameters by a specific type name instead of the keyword class or typename
- When a template is instantiated, the untyped parameter is replaced by a user-supplied or compiler-inferred value. These values must be constant expressions to allow the compiler to instantiate the template at compile time
template<unsigned N, unsigned M> int compare(const char (&p1)[N], const char (&p2)[M]) { return strcmp(p1, p2); } compare("hi", "mom");
- A non-type parameter can be an integer, or a pointer or left-value reference to an object or function type. Arguments bound to untyped integer parameters must be a constant expression. Arguments bound to pointers or referencing untyped parameters must have a static lifetime and cannot use a common (non-static) local variable or dynamic object as an argument to pointers or referencing untyped template parameters. A pointer parameter can also be an expression of nullptr or a constant value of 0
- Function templates can be declared inline or constexpr
template <typename T> inline T min(const T&, const T&);
- Our initial compare function, while simple, illustrated two important principles for writing generic code: the function parameter in the template is a reference to const; Conditional judgment in function body only applies to <operator
- Since only the <operator is applied, the function's requirement for the type to be processed is reduced, as long as the type supports the less than operator.
- In fact, it may be more type-independent and portable to define functions with less
template<typename T>int compare(const T &v1, const T &v2) { if(less<T>()(v1, v2)) return -1; if(less<T>()(v2, v1)) return 1; return 0; }
- When the compiler encounters a template definition, it does not generate code. The compiler generates code only when a specific version of the template is instantiated, and when we use (rather than defining) the template, the compiler generates code, which affects how we organize the code and when errors are detected.
- Generally, when we call a function, the compiler only needs to know the declaration of the function. Similarly, when we use objects of a class type, class definitions are required, but member functions do not have to be defined. Therefore, we put the class definitions and function declarations in the header file, while the definitions of normal and member functions in the source file
- Templates are different, and in order to generate an instantiated version, the compiler needs to know the definition of function templates or class template members. Therefore, unlike non-template code, template headers usually include both declarations and definitions
- The template designer should provide a header file that contains the template definition and declarations of all names used in the class template or member definition. The user must include the template header file and any type of header file used to instantiate the template
- Usually the compiler reports errors in three phases
- Compile the template itself: check for syntax errors
- When using templates: real parameter items, parameter types match, for class templates, check if the user has provided the correct number of template arguments
- When instantiating a template: Type-related errors are found, depending on how the compiler manages the instantiation, which may be reported only when linked
Class Template
- Class templates are used to generate blueprints for classes. Unlike function templates, the compiler cannot infer class templates and requires additional information from the user to replace the template argument list for template parameters.
#include <vector> #include <string> #include <initializer_list> #include <memory> template<typename T> class Blob { public: typedef T value_type; typedef typename std::vector<T>::size_type size_type; Blob(); Blob(std::initializer_list<T> il); size_type size() const { return data->size(); } bool empty()const { return data->empty(); } void push_back(const T &t) { data->push_back(t); } void push_back(T &&t) { data->push_back(std::move(t)); } T& back(); T& operator[](size_type i); private: std::shared_ptr<std::vector<T>> data; void check(size_type i, const std::string &msg) const; };
- We already know that using class templates requires additional information, which is an explicit list of template arguments that are bound to template parameters. The compiler uses these template arguments to instantiate specific classes
- Each instance of a class template forms a separate class, the type Blob<string>is not associated with any other Blob type, nor does it have special access to members of any other Blob type
- The member function of a class template is a common function, but each instance of a class template has its own version of the member function, so the member function of a class template has the same template parameters as the template. Therefore, member functions defined outside of a class template must start with the keyword template followed by a list of class template parameters.
- To define a member outside a class, you must specify which class the member belongs to, and the name of a class generated from a template must contain its template arguments. When we define a member function, the template actually participates in the same template parameters.
template <typename T> return_type Blob<T>::member_name(param_list)
template <typename T> void Blob<T>::check(size_type i, const std::string &msg) const { if(i >= data->size()) throw std::out_of_range(msg); } template <typename T> T& Blob<T>::back() { check(0,"back on empty Blob"); return data->back(); } template <typename T> T& Blob<T>::operator[](size_type i) { check(i,"subscript out of range"); return (*data)[i]; } template <typename T> Blob<T>::Blob() : data(std::make_shared<std::vector<T>>()){} template <typename T> Blob<T>::Blob(std::initializer_list<T> il): data(std::make_shared<std::vector<T>>(il)){}
// Constructor to instantiate Blob<int>and accept initialization lists Blob<int> squares = {0,1,2,3,4,5,6,7,8,9}; // Instantiate Blob<int>::size() const for(size_t i = 0; i != squares.size(); ++i) squares[i] = i * i; // Instantiation [] Operator
-
The Blob<int>class and its three member functions and a constructor are instantiated
-
If a member function is not used, it will not be instantiated and the member function will be instantiated only when it is used, which makes it possible to instantiate a class with that type even though it does not fully meet the requirements of a template operation
-
Template arguments must be provided when using a class template type, but there is one exception to this rule. In the scope of the class template itself, we can use the template name directly without providing arguments.
// Attempt to access an element that does not exist, class throws an exception template <typename T> class BlobPtr { public: 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]; } BlobPtr& operator++(); BlobPtr& operator--(); private: std::shared_ptr<std::vector<T>> check(std::size_t, const std::string&) const; std::weak_ptr<std::vector<T>> wptr; std::size_t curr; };
- Pre-incremental and decreasing members of BlobPtr return BlobPtr &, not BlobPtr<T>&. When we are in the scope of a class template, the compiler handles the template's own reference as if we had provided arguments that match the template parameters.
- When defining its members outside of a class template, it is important to remember that we are not in the scope of the class until we encounter a class name to indicate that we are in the scope of the class
template <typename T> BlobPtr<T> BlobPtr<T>::operator++(int) { BlobPtr ret = *this; ++*this; return ret; }
- Since the return type is outside the scope of the class, we must point out that the return type is an instantiated BlobPtr that uses the same type as the class instantiation. Within the body of the function, we have entered the scope of the class, so there is no need to duplicate the template arguments when defining ret. If no template arguments are provided, the compiler assumes that the types we use are identical to all the types instantiated by the member.
- Equivalent to
BlobPtr<T> ret = *this;
- Class Templates and Friends
- When a class contains a friend declaration, it is irrelevant whether the class and the friend are templates or not. If a class template contains a non-template friend, the friend is authorized to access all template instances. If a friend is a template, the class can be authorized to all friend template instances or to a specific instance
- The most common form of friendly relationship between a class template and another template is to establish a friendly relationship between the corresponding instance and its friends.
- In order to reference a specific instance of a template, we must first declare the template itself. A template declaration contains a list of template parameters
// Pre-declaration, declaring what friends need in a Blob template <typename> class BlobPtr; template <typename> class Blob; // Required for parameters in operator== template <typename T> bool operator==(const Blob<T>&, const Blob<T>&); template <typename T> class Blob { // Each Blob instance grants access to the same type of instantiated BlobPtr and the same operator friend class BlobPtr<T>; friend bool operator==<T> (const Blob<T>&, const Blob<T>&); };
- Friend declarations have template parameters for Blob s as their own template arguments. Friendship is therefore limited to blobs, blobPtrs, and equality operators instantiated with the same type.
- A class can also declare each instance of another template as its friend, or limit a specific instance to its friend
// Pre-declaration, declaring a specific instance of a template as a friend, is used to, template <typename T> class Pal; class C { friend class Pal<C>; // Pal instantiated with class C is a friend of C // All instances of Pal2 are friends of C, which does not require a predecessor template <typename T> friend class Pal2; }; template <typename T> class C2 { // Each instance of C2 declares the same instantiated Pal as a friend friend class Pal<T>; // Pal's template declaration must be in scope // All instances of Pal2 are friends of each instance of C2 and do not require a predecessor template <typename X> friend class Pal2; // Pal3 is a non-template class and is a friend of all C2 instances friend class Pal3; // No pre-declaration required };
- In the new standard, we can declare the template type parameter as a friend:
template <typename Type> class Bar { friend Type; };
- The new standard allows us to define a type alias for a class template
template<typename T> using twin = pair<T, T>; twin<string> authors; twin<int> win_loss; twin<double> area; // When we define a template type alias, we can fix one or more template parameters template <typename T> using partNo = pair<T, unsigned>; partNo<string> books; partNo<Student> kids;
- static member of class template
template <typename T> class Foo { public: static std::size_t count(){return ctr;} private: static std::size_t ctr; };
- In this code, Foo is a class template, a count's static member function, and a ctr's static data member. Each Foo instance has its own static member instance, that is, for any type of X, there is a Foo <X>:: CTR and a Foo <X>:: count member. All Foo<X>type objects share the same CTR object and count function
- Like other static data members, each static data member of a template class must have and only have one definition, but each instance of a class template has a unique static object. Therefore, similar to member functions that define templates, we define static data members as templates
template <typename T> size_t Foo<T>::ctr = 0; Foo<int> fi; // Instantiate Foo<int>class and static data member ctr auto ct = Foo<int>::count(); // Instantiate Foo<int>::count ct = fi.count(); // Use Foo<int>::count ct = Foo::count(); // Error. Which template instance is used for count?
- Like any other member function, a static member function is only instantiated when used
Template parameters
- Similar to the name of a function parameter, the name of a template parameter has no inherent meaning. We usually name a type parameter T, but actually we can use any name.
template <typename Foo> Foo cal(const Foo& a, const Foo& b) { Foo tmp = a; return tmp; }
- Template parameters follow the general scope rules, and the available range of a template parameter name is after its declaration and before the end of the template declaration or definition. Like any other name, the template parameter hides the same name declared in the outer scope. However, unlike most other contexts, template parameter names cannot be reused within a template
typedef double A; template <typename A, typename B> void f(A a, B b) { A tmp = a; // The tmp type is the type of template parameter A, not double double B; // Error, redeclare template parameter B }
- Because parameter names cannot be reused, a template parameter name can only appear once in a specific template parameter list
template <typename V, typename V> // error
- Template declarations must contain template parameters:
// Declare but do not define compare s and blobs template <typename T> int compare(const T&, const T&); template <typename T> class Blob;
- As with function parameters, template parameters in a declaration do not have to have the same name as in a definition
// All three clac s point to the same function template template <typename T>T clac(const T&, const T&); template <typename U>U clac(const U&, const U&); template <typename Type> Type clac(const Type& a. const Type& b){...}
-
Of course, every declaration and definition of a given template must have the same number and type of parameters
-
The declaration of all the templates required for a particular file is usually placed together at the beginning of the file, before any code that uses these templates
-
Assuming T is a template type parameter, when the compiler encounters code like T::mem, it does not know whether the MEM is a type member or a static data member until it is instantiated, but in order to process the template, the compiler must know whether the name represents an integer. For example, suppose T is the name of a type parameter when the compiler encounters T::size_type * p; It needs to know if we're defining a variable named P or if we're going to call it size_ The type static member is multiplied by the P variable.
-
By default, it is assumed that the name accessed through a scope operator is not a type. Therefore, if we want to use a type member for a template type parameter, we must explicitly tell the compiler that the name is a type.
template <typename T> typename T::value_type top(const T& c) { if(!c.empty()) return c.back(); else return typename T::value_type(); }
-
It uses typename to specify its return type and generates a value-initialized element to return to the caller when there are no elements in c
-
We can also provide default template arguments, and in the new standard we can provide default arguments for function and class templates.
// 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 &v1, const T &v2, F f = F()) { if(f(v1, v2)) return -1; if(f(v2, v1)) return 1; return 0; }
- We provide default arguments for this template parameter and for its corresponding function parameters. The default template argument indicates that compare will use the less function object class of the standard library, which is instantiated using the same type parameters as compare. The default function argument indicates that f will be a default initialized object of type F
bool i = compare(0,42); Sales_data item1(cin), item2(cin); bool j = compare(item1, item2, compareIsbn);
- In the second call, we pass three arguments to compare: compareIsbn and two Sales_ Object of type data. When three arguments are passed to compare, the type of the third argument must be a callable object, the return type of the callable object must be convertible to a bool value, and the accepted argument type must be compatible with the type of the first two arguments to compare. As always, the type of the template parameter is inferred from its corresponding function arguments. In this call, the type of T is inferred to Sales_data, F is inferred as the type of compareIsbn
- Whenever a class template is used, we must append <> to the template name, even if the class template provides default arguments for all template parameters, and we use these default arguments, you will need to follow the template name with an empty <>
template <class T = int> class Numbers { public: Numbers(T v = 0): val(v){} private: T val; }; Numbers<long double> lots_of_precision; Numbers<> average_precision;
Member Template
- A class can contain member functions that are templates themselves, which are called member templates and cannot be virtual functions
class DebugDelete { public: DebugDelete(std::ostream &s = std::cerr):os(s){} template <typename T> void operator()(T *p) const { os<<"deleting unique_ptr"<<std::endl; delete p; } private: std::ostream &os; };
- Like any other template, member templates start with a list of template parameters, and each DebugDelete object has an ostream member to write data and a member function that is itself a template that we can use instead of delete.
double *p = new double; DebugDelete d; d(p); // Call DebugDelete::operator()(double*) int *ip = new int; // Call operator()(int*) on a temporary DebugDelete object DebugDelete()(ip);
- Since calling a DebugDelete object delete s its given pointer, we can also use DebugDelete as unique_ Deletor for ptr, to overload unique_ptr's deletor, we give the deletor type in angle brackets, and provide this type of object to unqiue_ Constructor for PTR
// Destroy the object p refers to // Instantiate DebugDelete:: operator()<int> (int *) unique_ptr<int, DebugDelete> p(new int, DebugDelete()); unique_ptr<string, DebugDelete> sp(new string, DebugDelete());
- For class templates, we can also define member templates for them, in which case classes and members have their own. Stand-alone template parameters
template <typename T> class Blob { template <typename It> Blob(It b, It e); };
- Unlike normal members of class templates, member templates are function templates, and when we define a member template outside of a class template, we must provide a list of template parameters for both the class template and the member template, with the parameter list of the class template preceding and followed by the member's own template parameter list
template <typename T> template <typename It> Blob<T>::Blob(It b, It e): data(std::make_shared<std::vector<T>>(b,e)){}
- In order to instantiate a member template of a class template, we must provide both class and function template arguments. As always, on which object a member template is called, the compiler infers the argument of the class template parameter from the object type. As with a normal function template, the compiler usually infers its template argument from the function argument passed to the member template.
int ia[] = {0,1,2,3,4,5,6,7,8,9}; vector<long> vi = {0,1,2,3,4,5,6,7,8,9}; list<const char*> w = {"now","is","the","time"}; // Instantiate Blob<int>class and constructor that accepts two int*parameters Blob<int> a1(begin(ia),end(ia)); Blob<int> a2(vi.begin(),vi.end()); Blib<string> a3(w.begin(),w.end());
Control instantiation
- Templates are instantiated when they are used, which means that the same instance may appear in multiple object files, and each file has an instance of the template when two or more independently compiled header files use the same template and provide the same template parameters. This extra overhead can be severe on large systems and can be avoided by explicitly instantiating in the new standard
extern template declaration; // Instantiation declaration template declaration; // Instantiation Definition extern template class Blob<string>; // statement template int compare(const int&,const int &); // Definition
- When the compiler encounters an extern template declaration, it no longer generates instantiation code in this file. Declaring an instantiation as an extern implies a commitment to have a non-extern declaration (definition) of that instantiation elsewhere. For a given instantiation version, there may be multiple extern declarations, but only one must be defined.
- Since the compiler automatically instantiates a template when it is used, the declaration should appear before any code in this file that uses this instantiated version
// These template types must be initialized elsewhere in the program extern template class Blob<string>; extern template int compare(const int&,const int&); Blob<string> sa1,sa2; // Instantiation appears elsewhere // Blob<int>and its acceptance of initializer_list's constructor is instantiated in this file Blob<int> a1 = {0,1,2,3,4,5,6,7,8,9}; Blob<int> a2(a1); // Copy constructor instantiated in this file int i = compare(a1[0],a2[0]); // Instantiation appears elsewhere
template int compare(const int &,const int&); template class Blob<string>; // Instantiate all members of a class template
- An instantiated definition of a class template instantiates all members of the template, including inline member functions. When the compiler encounters an instantiated definition, it does not know which member functions the program uses. Therefore, unlike the webstroke instantiation that handles class templates, the compiler instantiates all members of the class, even if we do not use a member, it will be instantiated. So the type of a class template that we use to explicitly instantiate must be available to all members of the template
Efficiency and flexibility
- Shared_ The deletor type can be changed at any time during ptr's lifetime by passing it a callable object when creating or reset pointers. unique_ptr must provide the deletor type at definition time as an explicit template argument.
- Bind Deleter at Runtime
- shared_ptr may save managed pointers and deletor pointers in two members, with statements like this when destructing
del ? del(p) : delete p; // del(p) needs to jump to del's address at runtime
- Bind deletors at compile time
- unique_ptr may work by two template parameters, one representing the managed pointer and the other representing the type of deletor that is saved directly to unqiue_ In PTR object
// del binds at compile time, directly using deletors that call instantiation del(p); // No runtime overhead
template argument deduction
The process of determining a template argument from a function argument is called template argument inference
Type Conversion and Template Type Parameters
- If the type of a function parameter uses a template type parameter, it uses special initialization rules. Only a limited number of type conversions are automatically applied to these arguments. Compilers typically do not type-convert arguments, but instead generate a new template instance
- As always, the top const is ignored both in the parameter and in the parameter. In other type conversions, there are two items that can be applied to a function template in a call: const conversion: passing a reference or pointer to a non-const object to a reference or pointer parameter to a const; Conversion of an array or function pointer: If a function parameter is not a reference type, a normal pointer conversion can be applied to an array or function type argument. An array argument can be converted to a pointer to its first element; similarly, a function argument can be converted to a pointer to that function type
- Other type conversions, such as arithmetic conversions, derived-to-base class conversions, and user-defined conversions, cannot be applied to function templates
template <typename T> T fobj(T, T); // Arguments are copied template <typename T> T fref(const T&, const T&); // Quote string s1("a value"); const string s2("another value"); fobj(s1, s2); // Call fobj(string, string);const ignored fref(s1, s2); // Call Fref (const string &, const string &) int a[10], b[42]; fobj(a, b); // Call f(int*, int*); ferf(a, b); // Error, array type mismatch
long lng; conpare(lng, 1024); // Cannot instantiate compare(long, int); // Argument types can differ, but must be compatible template(typename A, typename B) int flexibleCompare(const A& v1, const B& v2) { if(v1 < v2) return -1; if(v2 < v1) return 1; return 0; } long lng; flexibleCompare(lng, 1024); // Call flexibleCompare(long, int)
Function Template Explicit Arguments
// The compiler cannot infer T1, it does not appear in the list of functions template <typename T1, typename T2, typename T3> T1 sum(T2, T3); // In this case, no type of function argument can be used to infer the type of T1, and the caller provides an explicit template argument for T1 each time the sum is called auto val3 = sum<long long>(i, lng); // Bad design, user must specify all three template parameters template <typename T1, typename T2, typename T3> T3 alternative_sum(T2, T1); // Error, cannot infer first few template parameters auto val3 = alternative_sum<long long>(i, lng); // Correct, explicitly specify three parameters auto val2 = alternative_sum<long long, int, long>(i, lng); long lng; compare(lng, 1024); // Error: Template parameter mismatch compare<long>(lng, 1024); // Correct, instantiate compare(long, long) compare<int>(lng, 1024); // Correct, instantiate compare(int, int), implement type conversion
Tail Return Type and Type Conversion
- We may want to write a function that accepts a reference to an element in the sequence returned by a pair of iterators representing the sequence, but we don't know the type of result, but we know that the required type is the element type of the sequence being processed. We can use decltype(*beg) to get the type of an element, but beg does not exist until the compiler encounters a list of parameters, so we must use a trailing return type
template <typename It> auto fcn(It beg, It end) -> decltype(*beg) { return *beg; }
- Returns the value of an element, not a reference. To get the element type, we can use the type conversion template from the standard library. These templates are defined in the header file type_traits.
- In this case, we can use remove_reference to get the element type. Remove_ The reference template has a template type parameter and a type member named type. If we instantiate remove_with a reference type Reference, the type will represent the type being referenced.
remove_reference<decltype(*beg)>::type;
template <typename It> auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type { // Processing Sequence return *beg; }
For Mod<T>, where Mod is | If T is | Mod<T>:: type is |
---|---|---|
remove_reference | X&or X& | X |
add_const | X & const X or function is T, otherwise | consf T |
add_lvalue_reference | X&& otherwise | X& T& |
add_rvalue_reference | X&or X& otherwise | T T&& |
remove_pointer | X* | X |
add_pointer | X& X&& otherwise | X* T* |
make_signed | unsigned X | X |
make_unsigned | Symbolized Type | unsigned X |
remove_extent | X[n] | X |
remove_all_extents | X[n1][n2] | X |
Function pointer and argument inference
- When we initialize a function pointer with a function template or assign values to a function pointer, the compiler uses the type of pointer to infer template arguments
template <typename T> int compare(const T&, const T&); // pf1 points to instance int compare (const int &, const int &) int (*pf1)(const int&, const int&) = compare;
- The type of parameter in pf1 determines the type of template argument for T. An error occurs if the template argument cannot be determined from the function pointer type
// Overloaded version of func, each version accepts a different function pointer type void func(int(*)(const string&, const string&)); void func(int(*)(const int&, const int&)); func(compare); // Error, which compare to use?
- The problem with this code is that the only type of template argument cannot be determined by the func's parameter type. A call to a func can accept either the int version or the string version, causing compilation to fail
- We can eliminate discrimination in func calls by using explicit templates
func(compare<int>);
Template argument inference and reference
template <typename T> void f1(T&); // Argument must be a left value f1(i); // T:int f1(ci); // T: const int f1(5); // error template <typename T> void f2(const T&); // Are inferred to be const int &, F2 de const independent of const in the actual parameter f2(i); f2(ci); f2(5); // Const&can bind right values template <typename T> void f3(T&&); f3(42);
- We may think that such a call to f3(i) is illegal, since I is a left value, we usually cannot bind a right value reference to the left value, but C++ defines two exceptions beyond the normal binding rules to allow this binding. This is the basis for the proper functioning of move as a standard library facility
- The first exception rule affects how the right-value reference parameter is inferred. When we pass a left value to a function's right-value reference parameter and the right-value reference points to a template type parameter, the compiler infers that the template type parameter is the left-value reference type of the argument. So the f3(i) call infers T to int&
- T infers to int&which seems to mean that the f3 function parameter should be a right-value reference of type int& Usually we cannot define a reference to a reference. However, it is possible to define indirectly by type aliases or by template type parameters
- In this case, we can use the second exception binding rule: if we indirectly create a reference to a reference, those references will collapse, and in all but one case they will collapse to a left-value reference. Only right-value references to right-value references collapse to right-value references
- Collapse X&, X& and X& into type X&
- X && &&Collapse to X &&
- These two rules lead to two important results: if a function parameter is a right-value reference to a template-type parameter, it can be bound to a left-value; And if the argument is a left value, the inferred template argument type will be a left value reference, and the function parameter will be instantiated as a normal left value reference parameter
- It is also worth noting that we can pass any type of argument to a function parameter of type T &&for which we can pass a right value and a left value
- Template parameters can be inferred that a reference type can have a significant impact on code within a template
template <typename T> void f3(T&& val) { T t = val; // Copy or Bind a Reference t = fcn(t); // Does assignment only change t or both T and val if(val == t){} // If T is a reference type, it will always be true }
- It may be helpful to handle this situation using a type conversion class. In practice, however, right-value references are often used in two situations: when a template forwards its arguments or when the template is overloaded
- It should be noted that overloading is often used for function templates that use right-value references
template <typename T> void f(T&&); // Bind a non-const right value template <typename T> void f(const T&); // Left and right const values
Understand std::move
// Typeename is also used in return types and type conversions template <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type&&>(t); }
- First, move's function parameter T& & is a right-value reference to a template type parameter that can be matched to any type of argument by reference collapse.
In std::move(string("bye"))
- Infer that the type of T is string
- remove_reference is instantiated with string
- Remove_ The type member of reference <string>is string
- The return type of move is string &&
- move's function parameter t is of type string &&
- Instantiated as: string & & move (string &&t)
- Function body returns static_ Cast <string &&> (t). Do Nothing
std::move(s1);
- Infer that the type of T is string&
- remove_reference is instantiated with string&
- type member is string
- move return type is string &&
- move's function parameter t is instantiated as string &&, collapsed as string&
- Instantiated as: string & & move (string &t)
- static_cast converts a left-value reference to a right-value reference
- Typically, static_cast can only be used for other legal type conversions, but there is a privileged rule for right-value references that can't be implicitly converted from left-value to right-value references, but can be explicitly converted
Forward
- Some functions need to forward one or more arguments to other functions without changing their type, in which case we need to preserve all the properties of the arguments being forwarded, including whether the argument type is const and whether the left or right value is
// flip1 is an incomplete implementation with missing top const and references template <typename F, typename T1, typename T2> void flip1(F f, T1 t1, T2 t2) { f(t2,t1); } // This function generally works fine, but there is a problem when calling a function that accepts a reference parameter void f(int v1, int &v2) { cout << v1 << " " << ++v2 << endl; }
- F changes the value of the argument bound to v2. However, if f is called through flip1, the changes made by F will not affect the arguments. The problem is that when flip1(f,j,42) is called, J is an int, not an int &, flip1 is instantiated as void flip1 (void (*fcn) (int, int &), int t1, int t2)
- The value of J is copied to t1, and the reference of f is bound to t1, not j, so that j is not affected
- By defining a function parameter as a right-value reference to a template type parameter, we can maintain all the type information for its corresponding argument. Instead, the use of reference parameters allows us to maintain the const attribute because consts are bottom-level in reference types. If we define a function parameter as T1 && T2 &&, we can preserve the left and right value properties of flipped arguments by reference collapse
template <typename F, typename T1, typename T2> void flip2(F f, T1 &&t1, T2 &&t2) { f(t2, t1); }
- This version solves half the problem. It works well for functions that accept a left-value reference, but it cannot be used for functions that accept a right-value reference parameter, for example:
void g(int &&i, int &j) { cout << i << " " << j << endl; } flip2(g, i, 42); // Error, cannot instantiate int&&from a left value
- What you pass to g is the t2 parameter in flip2, which, like the variable, is a left-value expression. So the right value flip2 passes to g refers to a left value of the parameter
- We can use a new standard library facility called forward to pass the parameters of flip2, which maintains the type of the original argument. Define in the utility header file. Must be called with explicit template arguments. Returns a right-value reference of an explicit argument type. Forward<T>Return T&
template <typename Type> intermediary(Type &&arg) { finalFcn(std::forward<Type>(arg)); // ... }
- In this example, we use Type as the explicit template argument type for forward, which is inferred from arg. Since arg is a right-value reference to a template type parameter, Type represents all type information for the arguments passed to arg.
- If the argument is a right value, Type is a normal type, forward<Type>returns Type &&.
- If the argument is a left value, collapsed by reference, the Type itself is a left value reference type. Fold the return type of forward<Type>by reference again, returning a left-value reference type
template <typename F, typename T1, typename T2> void flip(F f, T1 &&t1, T2 &&t2) { f(std::forward<T2>(t2), std::forward<T1>(t1)); }
Overload and Template
Function matching rules can be affected in several ways when function templates are involved
- For a call, its candidate function includes all template arguments inferring successful function template instances
- Candidate function templates are always possible because template argument inference excludes any impractical templates
- As always, viable functions are sorted by type conversion. Of course, there are very limited type conversions that can be used for function template calls
- As always, if exactly one function provides a better match than any other function, select this function, but if more than one function provides the same good match, then:
- If only one of the equally good functions is a non-template function, select this function
- Choose this template if the same good function has no non-template functions, has multiple function templates, and one of them is more specialized than the others
- Otherwise the call is ambiguous
// Print any type we can't handle template <typename T> string debug_rep(const T &t) { ostringstream ret; ret << t; return ret.str(); // Returns a copy of the ret-bound string } // Print the value of the pointer followed by the object pointed to by the pointer // Note: Not available for char* template <typename T> string debug_rep(T *p) { ostringstream ret; ret << "pointer: " << p; if(p) ret << " " << debug_rep(*p); else ret << " null pointer"; return ret.str(); } string s("hi"); cout << debug_rep(s) << endl; cout << debug_rep(&s) << endl;
- If debug_is called with a pointer Rep, both functions can generate viable instances:
- debug_rep(const string*&)
- debug_rep(string*)
- Second version debug_ An instance of rep is an exact match for this call, and the first version of the instance requires a conversion of a normal pointer to a const pointer.
// Consider another call const string *sp = &s; cout << debug_rep(sp) << endl;
- Both templates are workable and both match exactly
- debug_rep(const string*&)
- debug_rep(const string*)
- In this case, normal function matching rules cannot distinguish the two functions, but according to the special rules of overloaded functions, the call is resolved to debug_rep(T*), more specialized version
- Template debug_ Rep (const T&) can essentially be used for any type, including pointer types, which are more generic and can only be used for pointer types
// Print double quoted string string debug_rep(const string &s) { return '"' + s + '"'; } string s("hi"); cout << debug_rep(s) << endl;
- There are two equally good viable functions
- debug_rep<string>(const string&)
- debug_rep(const string&)
- When multiple function templates are equally good, the compiler chooses the most specialized version, and non-template functions are better than function templates
// Consider this call cout << debug_rep("hi world!") << endl;
- Three versions are available
- debug_rep(const T&), T -> char[10]
- debug_rep(T*) T -> const char
- Debug_ Rep(const string &) from const char* -> string
- Both templates provide exact matches -- the second template requires an array-to-pointer conversion, which is considered an exact match for function matching. A non-template version is possible, but requires a type conversion, so it is not an exact match. T* version is more specialized as before, compiler chooses it
- If we want to string the character pointer, we can define two other template overload versions
string debug_rep(char *p) { return debug_rep(string(p)); } string debug_rep(const char *p) { return debug_rep(string(p)); }
- It is worth noting that in order to make the debug_version of char* Rep works correctly, debug_when defining this version The declaration of rep(const string &) must be in scope, otherwise the wrong version may be called
template <typename T> string debug_rep(const T &t); template <typename T> string debug_rep(T *p); // The following declaration must be in scope string debug_rep(const string&); string debug_rep(char *p) { return debug_rep(string(p)); }
- Typically, if you use a function that you forget to declare, the code will fail to compile, but not for functions that overload function templates. If the compiler can instantiate a version from the template that matches the call.
Variable parameter template
- A variable parameter template is a template function or template class that accepts a variable number of parameters, which are called parameter packages. There are two kinds of parameter packages: template parameter packages represent 0 or more template parameters, function parameter packages represent 0 or more function parameters
- In a list of template parameters, class... or typename... indicates that the next parameter represents a list of 0 or more types, a type name followed by an ellipsis represents a list of 0 or more untyped parameters of a given type, and in a list of function parameters, if the type of a parameter is a template parameter package, the parameter is also a function parameter package.
// Args is a template parameter package, rest is a function parameter package // Args represents 0 or more template type parameters // rest represents 0 or more function parameters template <typename T, typename... Args> void foo(const T &t, const Args&... rest);
- Declares that foo is a variable parameter function template with a type parameter named T and a template parameter package named Args. This package represents 0 or more additional type parameters. foo's list of function parameters contains a const&type parameter, a type pointing to T, and a function parameter package named rest. This package represents 0 or more function parameters
- As always, the compiler infers the type of template parameter from the arguments to the function, and for a variable parameter template, the compiler 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"); foo(d, s); foo("hi"); // Empty package // The compiler instantiates four different versions of foo: 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]&); template <typename ... Args> void g(Args ... args) { cout << sizeof...(Args) << endl; cout << sizeof...(args) << endl; }
Writing variable parameter function templates
- We can use an initializer_list defines a function that accepts a variable number of arguments, but all arguments must have the same type. Variable parameter functions are useful when we don't know what type of arguments and numbers we want to work with
- Variable-parameter functions are usually recursive, with the first call processing the first argument in the package and calling itself with the remaining arguments. Our print function is also a pattern where each recursive call prints the second argument to the stream represented by the first argument. To terminate recursion, we also need to define a print function with a non-variable parameter that accepts a stream and an object
template<typename T> ostream &print(ostream &os, const T &t) { return os << t; } template <typename T, typename... Args> ostream &print(ostream &os, const T &t, const Args&... rest) { os << t << ", "; return print(os, rest...); }
- return print(os, rest...);
- Our variable parameter version of the print function accepts three parameters: an ostream&a const T&and a parameter package. This call passes only two arguments, resulting in the first argument in rest being bound to t, and the next print call's parameter package with the remaining arguments. Therefore, in each call, the first argument in the package is removed and becomes a bound t argument
- When defining print s for variable parameter versions, declarations for non-variable parameter versions must be in scope; otherwise, variable parameter versions will recurse indefinitely
packet extension
- For a parameter package, in addition to getting its size, the only thing we can do with it is extend. When you expand a package, we also provide a pattern for each extended element. Expanding a package is breaking it down into constituent elements, applying a pattern to each element, and getting an expanded list. We trigger the extension by placing an ellipsis to the right of the mode
- There are two extensions to the print function above
- The first extended operation extends the template parameter package to generate a list of function parameters for print. The second extension occurs in a call to print, which generates a list of arguments for the print call
- In the extension to Args, the compiler applies the pattern const Args&to each element in the template parameter package Args. Therefore, the result of extending this pattern is a comma-separated list of 0 or more types, each of which resembles a const type&
- print(cout, i, s, 42)
- Instantiated as: ostream &print (ostream &, const int &, const string &, const int &);
- Function parameter package extensions in print simply extend the package to its constituent elements, and the C++ language also allows for more complex extension patterns. For example, we can write a second variable parameter function that calls debug_for each argument Rep, then call print to print the result
// Call debug_on each argument in the print call Rep template <typename... Args> ostream &errorMsg(ostream &os, const Args&... rest) { return print(os, debug_rep(rest)...); }
- This print call uses the pattern debug_rep(rest). This pattern indicates that we want to call debug_for each element in the function parameter package rest Rep. The result of the extension is a comma-separated debug_rep Call List
- errorMsg(cerr, fcnName, code.num(), otherData, "other", item);
- print(cerr, debug_rep(fcnName), debug_rep(code.num()), debug_rep(otherData), debug_rep("other"), debug_rep(item));
- In contrast, the following pattern fails to compile
- print(os, debug_rep(rest...));
Forward Parameter Packet
- As we have seen, maintaining type information is a two-stage process. First, emplace_is required to maintain type information in arguments. The function parameter of back is defined as a right-value reference to a template type parameter
class StrVec { public: template <class... Args> void emplace_back(Args&&...); };
- Second, when emplace_ When back passes these arguments to the construct, we must use forward to preserve the original type of the argument
template <class... Args> inline void StrVec::emplace_back(Args&&... args) { chk_n_alloc(); alloc.construct(first_free++, std::forward<Args>(args)...); }
- It extends both the template parameter package Args and the function parameter package args, and this pattern generates elements in the following form
- std::forward<Ti>(ti)
- Variable-parameter functions usually forward their parameters to other functions, which typically have an emplace_ The same form as the back function.
template<typename... Args> void fun(Args&&... args) { work(std::forward<Args>(args)...); }
Template Specialization
// First version, comparing any two types template <typename T> int compare(const T&, const T&); // Handling string literal constants template <size_t N, size_t M> int compare(const char(&)[N], const char(&)[M]);
- Only when we pass a string literal constant or an array to compare, does the compiler call the version of the second untyped template parameter, and if passed to it a character pointer, the first version is called. Because pointers cannot be converted to references to arrays
- To handle character pointers, you can define a template specialization version for the first version of compare. A specialized version is a separate definition of a template in which one or more template parameters are specified as a specific type
- When we specialize a function template, we must provide arguments for each template parameter in the original template, and to indicate that we are instantiating a template, use the keyword template followed by a <>. Indicates that we will provide arguments for all template parameters
template <> int compare(const char* const &p1, const char* const &p2) { return strcmp(p1, p2); }
- We want to define a specialized version of this function, where T is const char*, and our function requires a reference to this type of const version. The const version of a pointer type is a constant pointer rather than a pointer to the const type. The type we need to use in the instantiation version is const char* const &, a reference to the const pointer of the const char
- The essence of specialization is to instantiate a template, not an overloaded function, so specialization does not affect function matching
- Templates and their specialization versions should be declared in the same header file, all declarations of templates with the same name should precede, followed by the specialization versions of these templates
- In addition to specializing function templates, we can also specialize class templates. for example
- A specialized hash class must be defined:
- An overloaded call operator that accepts an object of type container keyword and returns a size_t
- Two type members, result_type and argument_type, which calls the return type and parameter type of the operator, respectively
- Default constructor and copy assignment operator
- We can add members to the namespace, first we need to open the namespace
namespace std { template<> struct hash<Sales_data> { typedef size_t result_type; typedef Sales_data argument_type; size_t operator()(const Sales_data& s)const; }; size_t hash<Sales_data>::operator()(const Sales_data& s)const { return hash<string>()(s.bookNo) ^ hash<unsigned>()(s.units_sold) ^ hash<double>()(s.revenue); } }
- In this example, we give the standard library the job of defining a good hash function, XOR each data member generates a hash value, forming a complete hash value for a given object
- Assuming our specialized version is in this scope, when Sales_ This specialized version is used automatically by the compiler when data is the keyword type of the container
// Use hash<Sales_data>and Sales_ operator==in data unordered_multiset<Sales_data> SDset;
- Unlike function templates, a class template's specialization does not have to provide arguments for all template parameters. Instead, we can specify only a part of the template parameters, not all the template parameters, or only a part of the parameters, not all the attributes. A class template's partial specialization itself is a template, and when using it, the user must also provide arguments for template parameters that are not specified in the specialization version.
// Original Generic Version template <class T> struct remove_reference { typedef T type; }; // Partial specialization version, which will be used for left and right value references template <class T> struct remove_reference<T&> {typedef T type;}; template <class T> struct remove_reference<T&&> {typedef T type;};
- Since a partial specialization version is essentially a template, as always, we first define the template parameters, and like any other specialization version, the name of the partial specialization version is the same as the original template. For each template parameter of an incompletely determined type, there is a corresponding item in the template parameter list for the instantiated version. After the class name, we specify the arguments for the template parameters that we want to specialize, which are listed in angle brackets after the template name. The parameters that actually participate in the original template correspond by location.
- The template parameter list for some of the specialization versions is either a subset of the original template parameter list or a specialization version. In this example, the number of template parameters for the specialization version is the same as the original template, but the types are different, and the two specialization versions are used for left-value and right-value references, respectively.
- We can only specialize specific member functions instead of the entire template
template <typename T> struct Foo { Foo(const T &t = T()):mem(t){} void Bar(){} T mem; }; template<> void Foo<int>::Bar() { // Perform specialization for int s }
Glossary
- Class Template
- Default Template Arguments
- explicit instantiation
- explicitly specify
- Function parameter packages
- Function Template
- instantiation
- Example
- Member Template
- Untyped parameter
- packet extension
- Parameter Package
- Partial Specialization
- Pattern
- template argument
- template argument deduction
- Template parameters
- Template parameter list
- Template Parameter Package
- Template Specialization
- Type parameters
- Type Conversion
- Variable parameter template