本章节主要学习一下模板形参的基本知识。

模板参数有三种类型:类型模板参数、模板的模板参数(以模板作为模板的参数)、非类型模板参数。

类型模板参数

类型模板参数是我们使用模板的主要目的。也就是普通的类型模板参数,模板参数(Template parameters)声明在函数名之前的尖括号内:

template<typename T> // T是模板参数

我们可以定义一个或者多个类型模板参数:

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

也可以定义实例化后的类模板作为参数:

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

模板的模板参数

一个模板的参数是模板类型。

如果我们继续使用上面的类型版本参数也是可以做到类似于模板的模板参数,如:

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

实例化:

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

可以看到上面这样实例化时,需要指定int类型两次,而且这个类型是一样的,能不能实例化直接写成?

Stack<int, std::deque> intStack;

很显然是可以的,使用模板的模板参数,声明形式类似:

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

上面声明中的第二个模板参数Cont是一个类模板。注意声明中要用到关键字class(在C++17之后,模板的模板参数中的class也可以替换成typename)。只有类模板可以作为模板参数。这样就可以允许我们在声明Stack类模板的时候只指定容器的类型而不去指定容器中元素的类型。

看下面的例子加深理解该功能:

#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';
}

输出:

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

非类型模板参数

对于函数模板和类模板,模板参数并不局限于类型,普通值也可以作为模板参数。一般情况下,使用普通的类型模板参数定义了未确定类型的代码,直到调用时这些细节才真正确定,但有些情况我们需要一些值(不是类型)来实现这些细节,也就是基于值的模板。需要显示指定一些值才能对模板进行实例化。在上面我们的Stack例子的实现中,我们使用元素数目固定的数组来实现Stack。这个方法(用固定大小的数组)的优点是:无论是由你来亲自管理内存,还是由标准容器来管理内存,都可以避免内存管理开销。然而,决定一个栈(Stack)的最佳容量是很困难的。如果你指定的容量太小,那么栈可能会溢出;如果指定的容量太大,那么可能会不必要地浪费内存。一个好的解决方法就是:让栈的用户亲自指定数组的大小,并把它作为所需要的栈元素的最大个数。

template <typename T, int MAXSIZE>
 class Stack {
 private:
  T elems[MAXSIZE];    // 包含元素的数组
  int numElems;     // 元素的当前总个数
 public:
  Stack();        // 构造函数
  void push(T const&);  // 压入元素
    void pop();       // 弹出元素
  T top() const;     // 返回栈顶元素
  bool empty() const {  // 返回栈是否为空
    return numElems == 0;
  }
  bool full() const {  // 返回栈是否已满
    return numElems == MAXSIZE;
  }
 }; 

// 构造函数
template <typename T, int MAXSIZE>
Stack<T,MAXSIZE>::Stack ()
 : numElems(0)       // 初始时栈不含元素
{
  // 不做任何事情
}

函数模板也可以使用非类型模板参数:

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

非类型模板参数是有限制的。通常而言,它们包含以下四种类型:

  • 整数及枚举类型
  • 指针(对象指针或函数指针)
  • 引用(对象引用或函数引用)
  • 指向类对象成员函数的指针

浮点数和类对象(class-type)是不允许作为非类型模板参数的,如下:

template<double VAT>   //ERROR:浮点数不能作为非类型模板参数
double process (double v)
{
  return v * VAT;
}
template<std::string name>  //ERROR:类对象不能作为非类型模板参数
class MyClass {
 //...
};

之所以不能使用浮点数(包括简单的常量浮点表达式)作为模板实参是有历史原因的。然而,该特性的实现并不存在很大的技术障碍;因此,将来的C++版本可能会支持这个特性。由于字符串文字是内部链接对象(因为两个具有相同名称但处于不同模块的字符串,是两个完全不同的对象),所以你不能使用它们来作为模板实参:

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

MyClass<”hello”> x;   //ERROR:不允许使用字符串文字”hello”

另外,你也不能使用全局指针作为模板参数:

template <char const* name>
class MyClass {
};
char const* s = ”hello”;
MyClass<s> x;      //s是一个指向内部链接对象的指针

//然而,你可以这样使用:
template <char const* name>
class MyClass {
};

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

全局字符数组s由“hello”初始化,是一个外部链接对象。

模板参数作为返回值类型

template<typename T1, typename T2>
T1 max (T1 a, T2 b) {
    return b < a ? a : b;
}
...
auto m = ::max(4, 7.2);       // 注意:返回类型是第一个模板参数T1 的类型

上面的max模板函数的返回值类型总是T1。如果我们调用max(42, 66.66),返回值则是66。一般有三个方法解决这个问题:

  1. 引入专门的模板参数T3作为返回值类型
  2. 让编译器自动推导返回值类型
  3. 将返回值声明为两个模板参数的公共类型,比如int和float,公共类型就是float

引入额外模板参数作为返回值类型

template<typename T1, typename T2, typename RT>
RT max (T1 a, T2 b) {
    return b < a ? a : b;
}
...
auto m = ::max(4, 7.2);       // 注意:这里多了个返回类型,所以这里不能这样调用
auto m = ::max<int, double, double>(4, 7.2);       // 这样才行

由于RT类型是用作返回值,实例化时无法根据实参推导,所以必须显式指定直到最后一个不能确定的所有模板类型。比如上面例子中,RT的类型是不能通过参数类型推理来确定的,所以导致 T1 和 T2 的类型都要显式指定。当然你也可以把RT目标参数放前面,只需要指定RT,后面的T1和T2可以让编译器自动推导。但是这样就感觉奇奇怪怪的,不优雅!我们有更换的方法来定义模板函数的返回参数类型,请看下面。

让编译器推导返回值类型

一般定义函数有两种方式:

// 普通函数声明
return-type fun(...){...}
如:int fun(...){...}

// 用间接的方式来定义函数的返回类型
// C++11 增加的返回类型后置(trailing-return-type,又称跟踪返回类型)
// 主要是通过auto和decltype来推断出返回值的类型
auto fun(...)->return-type{...}
如:auto fun()->int{...}

那么对于模板函数,我们可以做到让编译器推导返回值类型而不需要定义一个专门的模板参数作为返回值:

使用decltype进行返回类型推导,无法返回模板参数
//这种方式无法做到
template<typename T1,typename T2>
decltype(a+b) fun(T1 a,T2 b){...}

//需要使用declval()进行辅助推导
template<typename T1,typename T2>
decltype(std::declval<T1>()+std::declval<T2>()) fun(T1 a,T2 b){...}

//但是上面太麻烦了,所以就加了个箭头
template<typename  T1,typename T2>
auto fun(T1 a, T2 b)->decltype(a+b){...}

当然如果你用的是C++14以上,那么就可以直接简单粗暴的使用auto就行了,因为从 C++14 开始⽀持仅⽤ auto 并实现返回类型推导,根本没decltype啥事了。

返回值声明为两个模板参数的公共类型

“只有你想不到的,没有C++做不到的”,哈哈。。。C++ 11中,标准库提供了一种对多个类型生成共同类型的方法。就是std::common_type。下面我们就来看下:

#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';
}

输出:

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

common_type作用就是返回参数列表中的参数都可以转换成的通用类型

当然也不是所有的类型都能有公共类型的,比如:

std::common_type<char, std::string>::type        // 报错,无法互相转换

小结

•模板可以具有值模板参数,而不仅仅是类型模板参数。

•对于非类型模板参数,你不能使用浮点数、class 类型的对象和内部链接对象(例如string)作为实参。

参考

《C++ Templates》