C++ Template Part 1: Function template

  • 2020-06-15 10:04:08
  • OfStack

Generic programming represented by Template is an important part of C++ language. I will make a systematic summary of my learning in the past six months through several blog articles. This article is the first part of the basic article.

Why generic programming

C++ is a strongly typed language, so it is impossible to write a common piece of logic like dynamic language (python javascript) that can pass in variables of any type. Generic programming makes up for this by designing general logic as a template, getting rid of type constraints, and providing an abstract mechanism other than inheritance that greatly improves code reusability.

Note: The template definition itself does not compile. Instead, the compiler generates code based on the type parameters provided by the user of the template when using the template. This process is called template instantiation. Different code is instantiated when the user provides different type parameters.

Function template definition

The function template is obtained by abstracting the common logic dealing with different types into functions.

Function templates can be declared as inline or constexpr and placed after template before the return value.

Ordinary function template

The following defines a function template called compare that supports multiple types of common comparison logic.


template<typename T>
int compare(const T& left, const T& right) {
  if (left < right) {
    return -1; 
  }
  if (right < left) {
    return 1; 
  }
  return 0;
}

compare<int>(1, 2); // Using template functions 

Member function template

Not only can ordinary functions be defined as templates, but also the member functions of a class can be defined as templates.


class Printer {
public:
  template<typename T>
  void print(const T& t) {
    cout << t <<endl;
  }
};

Printer p;
p.print<const char*>("abc"); // print abc

Why can't member function templates be virtual functions (virtual)?

This is because when c + + compiler parse1 class will determine the size of the vtable, if a virtual function is a function template is allowed, so compiler requires parse scanning all of the code before the class, find out the template of member function call (instantiate), and then to determine the size of the vtable, apparently this is not feasible, unless the change current compiler work mechanism.

The arguments that

For ease of use, in addition to specifying type parameters directly to a function template, we can also let the compiler infer type parameters from arguments passed to the function, a feature called template argument inference.

How to use


compare(1, 2); // inference T The type of int
compare(1.0, 2.0); // inference T The type of double
p.print("abc"); // inference T The type of const char*

Interestingly, you can also have the compiler infer template arguments based on the type of a function pointer by assigning a function template value to a function pointer of a specified type.


int (*pf) (const int&, const int&) = compare; // inference T The type of int

When the return value type is also a parameter

When the return type of a template function needs to be represented by another template parameter, you cannot use argument inference to get all the type parameters. There are two solutions:

The return value type is completely independent of the parameter type, so the specified return value type needs to be displayed, and the rest is left to argument inference.

Note: This behavior is the same as the default argument of the function, and we must specify it 1 from left to right.


template<typename T1, typename T2, typename T3>
T1 sum(T2 v2, T3 v3) {
 return static_cast<T1>(v2 + v3);
}

auto ret = sum<long>(1L, 23); // The specified T1, T2 and T3 Leave it to the compiler to infer 

template<typename T1, typename T2, typename T3>
T3 sum_alternative(T1 v1, T2 v2) {
 return static_cast<T1>(v1 + v2);
}
auto ret = sum_alternative<long>(1L, 23); //error You can only drive from left to right 1 The specified 
auto ret = sum_alternative<long,int,long>(1L,23); //ok,  Who told you to put the last 1 a T3 What about as a return type? 

The return value type can be obtained from the argument type, so writing the function as a tail-return type is a pleasant way to use argument inference.


template<typename It>
auto sum(It beg, It end) -> decltype(*beg) {
 decltype(*beg) ret = *beg;
 for (It it = beg+1; it != end; it++) {
   ret = ret + *it;
 }
 return ret;
}

vector<int> v = {1, 2, 3, 4};
auto s = sum(v.begin(), v.end()); //s = 10

Automatic type conversion at argument inference

The compiler usually does not cast arguments when it makes template argument inference, except in the following cases:

Ordinary objects assign values to const reference int a = 0; - > const T & Array name converted to header pointer int a[10] = {0}; - > T* Function name converted to function pointer void func(int a){... } - > T*

Function template overloading

Between function templates and between function templates and normal functions can be overloaded. The compiler can handle the most specific version of this type based on the function arguments provided at the time of the call. In terms of particularity, generally consider in the following order:

Common function Special templates (defined as T, Pointers, references, containers, etc.) Normal templates (no restrictions on T)

As for how to determine if a template is more specific, the principle is as follows: if all instances of the template B can instantiate the template A, and vice versa, then B is more specific than A.


template<typename T>
void func(T& t) { // Universal template function 
  cout << "In generic version template " << t << endl;
}

template<typename T>
void func(T* t) { // Pointer to the version 
  cout << "In pointer version template "<< *t << endl;
}

void func(string* s) { // Common function 
  cout << "In normal function " << *s << endl;
}

int i = 10;
func(i); // Calling the generic version, other functions either cannot be instantiated or do not match 
func(&i); // Call pointer version, generic version may also be used, but the compiler selects the most specific version 
string s = "abc";
func(&s); // Calls to normal functions, generic versions, and special versions are also available, but the compiler chooses the most specialized version 
func<>(&s); // Call pointer version through <> Tell the compiler what we need to do template It's not a normal function 

Template function specialization

Sometimes generic function templates don't solve individual types of problems, and we have to customize them. This is called function template specialization. Function template specialisation must specify all template parameters.


template<>
void func(int i) {
  cout << "In special version for int "<< i << endl; 
}

int i = 10;
func(i); // Call the specialized version 

Related articles: