C + + learning notes: template parameters

Posted by llandudno on Fri, 11 Feb 2022 21:20:18 +0100

This chapter mainly studies the basic knowledge of template formal parameters.

There are three types of template parameters: type template parameters, template parameters of templates (taking templates as template parameters), and non type template parameters.

Type template parameters

Type template parameters are the main purpose of using templates. That is, ordinary type template parameters. Template parameters are declared in angle brackets before the function name:

template<typename T> // T is the template parameter

We can define one or more type template parameters:

template <typename T1, typename T2> 
class Stack {
private:
  Cont elems; // elements
  // ......
};

You can also define the instantiated class template as a parameter:

template <typename T, typename Cont = std::vector<T>> 
class Stack {
private:
  Cont elems; // elements
  // ......
};

Template parameters of template

The parameter of a template is the template type.

If we continue to use the above type version parameters, we can also achieve template parameters similar to templates, such as:

template <typename T, typename Cont = std::vector<T>> 
class Stack {
private:
  Cont elems; // elements
  // ......
};

Instantiation:

Stack<int, std::deque<int>> intStack;

You can see that when instantiating like this above, you need to specify the int type twice, and this type is the same. Can you instantiate it and write it directly?

Stack<int, std::deque> intStack;

Obviously, you can use the template parameters of the template. The declaration form is similar to:

template<typename T, template<typename Elem> class Cont = std::vector>
class Stack {
private:
  Cont<T> elems; // elements
  // ......
};

The second template parameter Cont in the above declaration is a class template. The parameter in the C + + template can be replaced by the keyword in the C + + template. Only class templates can be used as template parameters. This allows us to specify only the type of container and not the type of elements in the container when declaring the Stack class template.

Look at the following example to deepen your understanding of this function:

#include <iostream>
#include <vector>
#include <deque>
#include <cassert>
#include <memory>

template<typename T,
    template<typename Elem,
             typename = std::allocator<Elem>>
    class Cont = std::deque>
class Stack {
private:
    Cont<T> elems; // elements
public:
    void push(T const&); // push element
    void pop();  // pop element
    T const& top() const;  // return top element
    bool empty() const // return whether the stack is empty
    {
        return elems.empty();
    }

    // assign stack of elements of type T2
    template<typename T2, 
        template<typename Elem2,
                 typename = std::allocator<Elem2>>
        class Cont2>
    Stack<T, Cont>& operator=(Stack<T2, Cont2> const&);

    // to get access to private members of any Stack with elements of type T2:
    template<typename, template<typename, typename> class>
    friend class Stack;
};

template<typename T, template<typename, typename> class Cont>
void Stack<T,Cont>::push(T const& elem) {
    elems.push_back(elem); // append copy of passed elem
}

template<typename T, template<typename, typename> class Cont>
void Stack<T,Cont>::pop() {
    assert(!elems.empty());
    elems.pop_back();  // remove last element
}

template<typename T, template<typename, typename> class Cont>
T const& Stack<T,Cont>::top() const {
    assert(!elems.empty()); 
    return elems.back();    // return last element
}

template<typename T, template<typename, typename> class Cont>
    template<typename T2, template<typename, typename> class Cont2>
Stack<T,Cont>& Stack<T,Cont>::operator=(Stack<T2,Cont2> const& op2) {
    elems.clear();   // remove existing elements
    elems.insert(elems.begin(), 
                 op2.elems.begin(),
                 op2.elems.end());
    return *this;
}

int main() {
    Stack<int> iStack;  // stacks of ints
    Stack<float> fStack; // stacks of floats

    // manipulate int stack
    iStack.push(1);
    iStack.push(2);
    std::cout << "iStack.top(): " << iStack.top() << '\n';

    // manipulate float stack
    fStack.push(3.3);
    std::cout << "fStack.top(): " << fStack.top() << '\n';
    
    // assign stack of different type and manipulate again
    fStack = iStack;
    fStack.push(4.4);
    std::cout << "fStack.top(): " << fStack.top() << '\n';

    // stack for doubles using a vector as an internal container
    Stack<double, std::vector> vStack;
    vStack.push(5.5);
    vStack.push(6.6);
    std::cout << "vStack.top(): " << vStack.top() << '\n';

    vStack = fStack;
    std::cout << "vStack: ";
    while(!vStack.empty()) {
        std::cout << vStack.top() << ' ';
        vStack.pop();
    }
    std::cout << '\n';
}

Output:

iStack.top(): 2
fStack.top(): 3.3
fStack.top(): 4.4
vStack.top(): 6.6
vStack: 4.4 2 1 

Non type template parameters

For function templates and class templates, template parameters are not limited to types, and common values can also be used as template parameters. In general, the code of undetermined type is defined by using ordinary type template parameters, and these details are not really determined until calling. However, in some cases, we need some values (not types) to implement these details, that is, value based templates. You need to specify some values in order to instantiate the template. In the implementation of our Stack example above, we use an array with a fixed number of elements to implement Stack. The advantage of using this method is that you can manage the memory by the standard or by the fixed memory container. However, it is difficult to determine the optimal capacity of a Stack. If the capacity you specify is too small, the Stack may overflow; If the specified capacity is too large, memory may be unnecessarily wasted. A good solution is to let the Stack user personally specify the size of the array and take it as the maximum number of Stack elements required.

template <typename T, int MAXSIZE>
 class Stack {
 private:
  T elems[MAXSIZE];    // Array containing elements
  int numElems;     // Current total number of elements
 public:
  Stack();        // Constructor
  void push(T const&);  // Push in element
    void pop();       // Pop up element
  T top() const;     // Return stack top element
  bool empty() const {  // Returns whether the stack is empty
    return numElems == 0;
  }
  bool full() const {  // Returns whether the stack is full
    return numElems == MAXSIZE;
  }
 }; 

// Constructor
template <typename T, int MAXSIZE>
Stack<T,MAXSIZE>::Stack ()
 : numElems(0)       // Initially, the stack does not contain elements
{
  // Don't do anything
}

Function templates can also use non type template parameters:

template<typename T, int VAL>
T addValue(T const& x)
{
  return x + VAL;
}

Non type template parameters are limited. Generally speaking, they include the following four types:

  • Integer and enumeration types

  • Pointer (object pointer or function pointer)

  • Reference (object reference or function reference)

  • Pointer to member function of class object

Floating point numbers and class type objects are not allowed as non type template parameters, as follows:

template<double VAT>   //ERROR: floating point numbers cannot be used as non type template parameters
double process (double v)
{
  return v * VAT;
}
template<std::string name>  //ERROR: class object cannot be used as a non type template parameter
class MyClass {
 //...
};

There are historical reasons why floating-point numbers (including simple constant floating-point expressions) cannot be used as template arguments. However, there is no great technical obstacle to the realization of this feature; Therefore, future versions of C + + may support this feature. Since string literals are internal linked objects (because two strings with the same name but in different modules are two completely different objects), you cannot use them as template arguments:

template<char const* name>
class MyClass {
 //...
};

MyClass<"hello"> x;   //ERROR: string literal 'hello' is not allowed

In addition, you cannot use global pointers as template parameters:

template <char const* name>
class MyClass {
};
char const* s = "hello";
MyClass<s> x;      //s is a pointer to an internally linked object

//However, you can use this:
template <char const* name>
class MyClass {
};

extern char const s[] = "hello";
MyClass<s> x;    //OK

The global character array s is initialized by "hello" and is an external link object.

Template parameter as return value type

template<typename T1, typename T2>
T1 max (T1 a, T2 b) {
    return b < a ? a : b;
}
...
auto m = ::max(4, 7.2);       // Note: the return type is the type of the first template parameter T1

The return value type of the above Max template function is always T1. If we call max(42, 66.66), the return value is 66. There are generally three ways to solve this problem:

  1. Introduce a special template parameter T3 as the return value type

  2. Let the compiler automatically deduce the return value type

  3. Declare the return value as the public type of two template parameters, such as int and float. The public type is float

Introduce additional template parameters as return value types

template<typename T1, typename T2, typename RT>
RT max (T1 a, T2 b) {
    return b < a ? a : b;
}
...
auto m = ::max(4, 7.2);       // Note: there are multiple return types here, so you can't call it like this
auto m = ::max<int, double, double>(4, 7.2);       // That's it

Since the RT type is used as the return value and cannot be derived from the arguments during instantiation, all template types that cannot be determined until the last one must be explicitly specified. For example, in the above example, the type of RT cannot be determined by parameter type reasoning, so the types of T1 and T2 should be explicitly specified. Of course, you can also put the RT target parameter in front, just specify RT, and the following T1 and T2 can be deduced by the compiler automatically. But it feels strange and not elegant! We have a replacement method to define the return parameter type of the template function. Please see the following.

Let the compiler deduce the return value type

There are two general ways to define functions:

// Ordinary function declaration
return-type fun(...){...}
For example: int fun(...){...}

// Define the return type of a function indirectly
// C++11 added trailing return type (also known as tracking return type)
// It mainly infers the type of return value through auto and decltype
auto fun(...)->return-type{...}
For example: auto fun()->int{...}

For template functions, we can let the compiler deduce the return value type without defining a special template parameter as the return value:

use decltype The template parameters cannot be returned when the return type derivation is performed
//This cannot be done
template<typename T1,typename T2>
decltype(a+b) fun(T1 a,T2 b){...}

//declval() is required for auxiliary derivation
template<typename T1,typename T2>
decltype(std::declval<T1>()+std::declval<T2>()) fun(T1 a,T2 b){...}

//But it was too troublesome, so an arrow was added
template<typename  T1,typename T2>
auto fun(T1 a, T2 b)->decltype(a+b){...}

Of course, if you use C++14 or above, you can simply and rudely use auto, because starting from C++14, you can only use auto and realize the derivation of return type. There is nothing to decltype at all.

The return value is declared as a public type of two template parameters

"Only what you can't think of, nothing C + + can't do", ha ha... In C++ 11, the standard library provides a method to generate common types for multiple types. std::common_type. Let's take a look at the following:

#include <iostream>
#include <type_traits>
 
template <class T>
struct Number { T n; };
 
template <class T, class U>
Number<typename std::common_type<T, U>::type> 
operator+(const Number<T>& lhs, const Number<U>& rhs) 
{
    return {lhs.n + rhs.n};
}
 
int main()
{
    Number<int> i1 = {1}, i2 = {2};
    Number<double> d1 = {2.3}, d2 = {3.5};
    std::cout << "i1i2: " << (i1 + i2).n << "\ni1d2: " << (i1 + d2).n << '\n'
              << "d1i2: " << (d1 + i2).n << "\nd1d2: " << (d1 + d2).n << '\n';
}

Output:

i1i2: 3
i1d2: 4.5
d1i2: 4.3
d1d2: 5.8

common_ The function of type is to return the general type that all parameters in the parameter list can be converted to

Of course, not all types can have public types, such as:

std::common_type<char, std::string>::type        // An error is reported and cannot be converted to each other

Summary

• templates can have value template parameters, not just type template parameters.

• for non type template parameters, you cannot use floating-point numbers, objects of type class and internal link objects (such as string) as arguments.

reference resources

<C++ Templates>

Topics: C++ Back-end Cpp