C++ Template-5 基础技术

本章介绍了模板使用时的一些基础技术:关键字 typename 的使用,将成员函数和嵌套类定义成模板,模板参数模板(template template parameters),零初始化和一些关于使用字符串常量作为函数模板参数的细节。

不是本系列第一篇文章,推荐阅读完前面的文章再看本文,以下是本系列文章目录。

1. 函数模板

2. 类模板

3. 非类型模板参数

4. 可变参数模板

5. 基础技术(本文)

6. 移动语意与 enable_if<>

5. 基础技术

本章节包含一些关于模板实际使用中的进一步的基础知识:关键字 typename 的使用,将成员函数和嵌套类定义成模板,模板参数模板(template template parameters),零初始化和一些关于使用字符串常量作为函数模板参数的细节。这些方面可能有时是一些小技巧,但是作为一个C++日常程序员,应该了解过它们。

5.1 关键字 typename

关键字 typename 在 C++ 标准化过程中被引入进来用来说明模板内部的标识符是一个类型。考虑如下的一个例子:

1
2
3
4
5
6
7
8
9
10
template<typename T>
class MyClass
{
public:
...
void foo()
{
typename T::SubType* ptr;
}
};

这里,第二个 typename 是用来说明 SubType 是一个在 class T 中定义的一个类型。因此,ptr 是一个指向 T::SubType 类型的一个指针。

不通过 typename , SubType 会被假设成一个非类型成员(e.g. 一个静态数据成员或是一个枚举常量)。因此,表达式

1
T::SubType* ptr

会被解释成 class T 中的静态 SubType 成员与 ptr 的乘法,这并不是一个错误,因为对于 MyClass<> 的某些实例化版本来说,这份代码是有效的。

通常而言,当一个模板类型参数是类型时,必须使用 typename 来修饰,在 13.3.2 节(未写)会介绍。

typename 的一种应用是用来声明泛型代码标准容器的迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

// print elements of an STL container
template<typename T>
void printcoll (T const& coll)
{
typename T::const_iterator pos; // iterator to iterate over coll
typename T::const_iterator end(coll.end()); // end position
for (pos=coll.begin(); pos!=end; ++pos)
{
std::cout << *pos << ' ';
}
std::cout << '\n';
}

在这个函数模板中,调用参数是 T 类型的一个标准容器。为了遍历容器内的所有元素,使用了容器中的 iterator 类型,即在每个标准容器类中声明的 const_iterator 类型:

1
2
3
4
5
6
7
class stlcontainer
{
public:
using iterator = ...; // iterator for read/write access
using const_iterator = ...; // iterator for read access
...
};

因此,为了调用 T 类型中的 const_iterator 类型,你必须在其前面加上 typename 关键字:

1
typename T::const_iterator pos;

关于 C++17 之前 typename 的使用,参见 13.3.2 节(未写)。注意,C++20 后可能取消在一些通用场合中 typename 的使用。

5.2 零初始化

对于一些基础类型,比如 int, double,或者指针类型,他们没有默认的构造将它们初始化成一个有用的默认值,任何没有被初始化的本地变量有一个没有确定的值。

1
2
3
4
5
void foo()
{
int x; // x has undefined value
int* ptr; // ptr points to anywhere(instead of nowhere)
}

因此在定义模板时,如果想让模板类型的变量全被默认值初始化,如下的定义时无效的,对于内置类型(built-in type),它们不会被初始化

1
2
3
4
5
template<typename T>
void foo()
{
T x; // x has undefined value if T is built-in type
}

由于这个原因,最好显式地调用类型的默认构造函数将他们初始化为 0(或者 false 对于 bool 类型,或 nullptr 对于指针类型)。因此,通过如下的写法你就可以保证内置类型也能被合适地初始化。

1
2
3
4
5
template<typename T>
void foo()
{
T x{}; // x is zero(or false or nullptr) if T is a built-in type
}

这种初始化方式叫做值初始化,它可以调用提供的构造函数或者零初始化来初始化一个对象。即使有显式的构造函数也适用。

在 C++11 之前,确保正确初始化的语法是:

1
T x = T(); // x is zero(or false) if T is a built-in type

在 C++17 之前,只有在拷贝构造函数没有被显式地调用时才有效。在 C++17 中,由于强制拷贝省略(mandatory copy elision)的使用,这一限制被解除了,可以使用初始化列表的构造方式即使这个对象没有默认的构造函数。

为了确保类模板已被特例化和初始化的类型,你可以定义一个默认的运用初始化列表的构造器:

1
2
3
4
5
6
7
8
9
template<typename T>
class MyClass
{
private:
T x;
public:
MyClass(): x{}{} // ensures that x is initialized even for built-in types
...
}

C++11 之前的语法

1
MyClass():x(){} // ensures that x is initialized even for built-in types

也仍然有效。

从 C++11 开始,你也可以对费静态成员提供一个默认的初始化,因此下面的代码也是可行的:

1
2
3
4
5
6
7
template<typename T>
class MyClass
{
private:
T x{}; // zero-initialize x unless otherwise specified
...
}

但是,注意这种语法不能用于默认参数,例如

1
2
template<typename T>
void foo(T p{}){...} // ERROR

所以,我们必须这么写:

1
2
template<typename T>
void foo(T p = T{}){...} // OK(must use T() before C++11)

5.3 使用 this->

对于继承了使用模板参数的基类的类模板,使用 x 不一定都等价于 this->x,尽管成员 x 是继承的。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T>
class Base
{
public:
void bar();
};

template<typename T>
class Derived : Base<T>
{
public:
void foo()
{
bar(); // calls external bar() or error
}
};

在这个例子中,在 Derivedfoo() 使用的 bar() 不会被调用为 Base 中的 bar() 函数。因此,这段代码要么是错误的,要么调用了另外一个 bar() (比如全局中的 bar())。

我们会在 13.4.2 节(未写)详细讨论这个问题。现在,依据经验,当使用基类的任何一个依赖于模板参数的成员时,请使用 this->Base<T>::

5.4 原始数组和字符串字面量模板

当给模板传递原始数组或者是字符串字面量是,你需要注意这些。首先,如果模板参数被定义为引用,参数不会被退化(decay)。因此,传递变量 "hello" 的类型是 char const[6]。这在传递不同长度的原始数组或者是字符串时可能会出现问题,因为它们的类型是不同的。仅当按值传递参数时,类型才会退化,因此字符串常量的类型会被转化为 char const*。在 第 7 节(未写)会详细讨论这个问题。

注意,你也可以提供一个模板专门处理原始数组和字符串,例如:

1
2
3
4
5
6
7
8
9
template<typename T, int N, int M> 
bool less (T(&a)[N], T(&b)[M])
{
for (int i = 0; i < N && i < M; ++i) {
if (a[i] < b[i]) return true;
if (b[i] < a[i]) return false;
}
return N < M;
}

这里,当使用

1
2
3
int x[] = {1, 2, 3};
int y[] = {1, 2, 3, 4, 5};
std::cout << less(x, y) << '\n';

less<>() 中的 T 被实例化为 intN 被实例化为 3M 被实例化为 5

你也可以将此模板运用于字符串常量:

1
std::cout << less("ab", "abc") << '\n';

在这个例子中,less<>() 中的 T 被实例化为 char constN 被实例化为 3M 被实例化为 4

当你只想为字符串常量(或者其他 char 数组)提供专门函数模板,你可以这么做。

1
2
3
4
5
6
7
8
9
template<int N, int M> 
bool less (char const(&a)[N], char const(&b)[M])
{
for (int i = 0; i < N && i < M; ++i) {
if (a[i] < b[i]) return true;
if (b[i] < a[i]) return false;
}
return N < M;
}

注意有时你必须为一些边界未定的数字去重载或者偏特化。下面的程序展示了对数组所有可用的重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>

template<typename T>
struct MyClass; // primary template

template<typename T, std::size_t SZ>
struct MyClass<T[SZ]> // partial specialization for arrays of known bounds
{
static void print() { std::cout << "print() for T[" << SZ << "]\n"; }
};

template<typename T, std::size_t SZ>
struct MyClass<T(&)[SZ]> // partial spec. for references to arrays of known bounds
{
static void print() { std::cout << "print() for T(&)[" << SZ << "]\n"; }
};

template<typename T>
struct MyClass<T[]> // partial specialization for arrays of unknown bounds
{
static void print() { std::cout << "print() for T[]\n"; }
};

template<typename T>
struct MyClass<T(&)[]> // partial spec. for references to arrays of unknown bounds
{
static void print() { std::cout << "print() for T(&)[]\n"; }
};

template<typename T>
struct MyClass<T*> // partial specialization for pointers
{
static void print() { std::cout << "print() for T*\n"; }
};

这里类模板 MyClass<> 被特例化为不同的数据类型:已知或未知边界的数组或数组引用,还有指针。每个例子都有所不同而且在使用中都会出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "arrays.hpp"

template<typename T1, typename T2, typename T3>
void foo(int a1[7], int a2[], // pointers by language rules
int (&a3)[42], // reference to array of known bound
int (&x0)[], // reference to array of unknown bound
T1 x1, // passing by value decays
T2& x2, T3&& x3) // passing by reference
{
MyClass<decltype(a1)>::print(); // uses MyClass<T*>
MyClass<decltype(a2)>::print(); // uses MyClass<T*>
MyClass<decltype(a3)>::print(); // uses MyClass<T(&)[SZ]>
MyClass<decltype(x0)>::print(); // uses MyClass<T(&)[]>
MyClass<decltype(x1)>::print(); // uses MyClass<T*>
MyClass<decltype(x2)>::print(); // uses MyClass<T(&)[]>
MyClass<decltype(x3)>::print(); // uses MyClass<T(&)[]>
}

int main()
{
int a[42];
MyClass<decltype(a)>::print(); // uses MyClass<T[SZ]>

extern int x[]; // forward declare array
MyClass<decltype(x)>::print(); // uses MyClass<T[]>

foo(a, a, a, x, x, x, x);
}

int x[] = {0, 8, 15}; // define forward-declared array

注意根据语法规则,被声明为数组(带或不带长度)的调用参数,它们的真实类型是指针。同样注意模板对于未知边界的数组可以使用一个不完整的类型,比如:

1
extern int i[];

当这个参数被引用传递时,它的类型是 int(&)[],这个类型也能作为模板参数。

19.3.1 节(未写)会介绍另一个使用不同数组类型的泛型代码。

5.5 成员模板

类成员也可以是模板。对于嵌套类和成员函数也是一样。这一功能的应用与优点同样能用在 Stack<> 类模板中。一般地,你也对于两个同一类型的 stack 互相赋值(即它们的元素的类型也是相同的)。但是,你不能用对两个不同类型的 stack 互相赋值,即使它们的类型可以隐式转换。

1
2
3
4
5
Stack<int>   intStack1, intStack2; // stacks for ints
Stack<float> floatStack; // stack for floats
...
intStack1 = intStack2; // OK: stacks have same type
floatStack = intStack1; // ERROR:stacks have different type

默认赋值运算符需要等式两边的类型相同,因此如果两个 stack 的类型不同的话,这一条件不满足。

通过为模板定义一个赋值运算符,你就可以对两个不同类型的 stack 互相赋值。你可以如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T>
class Stack
{
private:
std::deque<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>
Stack& operator= (Stack<T2> const&);
};

以上代码有两处变化:

  1. 我们为 stack 提供了一个类型为 T2 的赋值运算符
  2. 此 stack 使用了 std::deque<> 作为元素的容器,这是为了方便新的赋值运算符的实现。

新的赋值运算符如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2)
{
Stack<T2> tmp(op2); // create a copy of the assigned stack

elems.clear(); // remove existing elements
while (!tmp.empty()) { // copy all elements
elems.push_front(tmp.top());
tmp.pop();
}
return *this;
}

首先我们看一下定义成员模板的语法。在模板参数为 T 的模板中,定义了一个模板参数为 T2 的内部模板:

1
2
3
template<typename T>
template<typename T2>
...

在这个成员函数中,你可能期望对于所有 stack 类型的 op2 ,都能简单地访问到所有需要的信息。但是,stack 可能有多个不同的类型(如果你实例化类模板的参数类型不同,你会获得不同类型的类),因此你被受限于使用公共的接口。这样访问元素的唯一方法就是调用 top() 函数。因此,每个元素都必须相继出现在栈顶。这就要求 op2 首先要被拷贝一遍,然后对这个拷贝调用 pop() 去访问所有元素。因为 top() 返回的是最后一个压入栈的元素,我们可能更乐意使用一个可以在两段添加元素的容器。因此,我们使用了 std::deque<>,提供了 push_front() 将元素添加到另一端。

为了访问 op2 的所有成员,你可以把其他 stack 声明为友元:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T>
class Stack
{
private:
std::deque<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>
Stack& operator= (Stack<T2> const&);
// to get access to private members of \TStack<T2> for any type T2:
template<typename> friend class Stack;
};

正如你所见,因为模板参数的名称没有被使用,所以可以省略:

1
template<typename> friend class Stack;

这样,下面的模板赋值运算符的实现是有效的:

1
2
3
4
5
6
7
8
9
10
template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2)
{
elems.clear(); // remove existing elements
elems.insert(elems.begin(), // insert at the beginning
op2.elems.begin(), // all elements from op2
op2.elems.end());
return *this;
}

无论你采用哪种实现方式,通过这个成员模板,你可以将 int 类型的 stack 赋值给 float 类型的 stack:

1
2
3
4
5
从 Stack<int>   intStack;   // stack for ints
Stack<float> floatStack; // stack for floats
...
floatStack = intStack; // OK: stacks have different types,
// but int converts to float

当然,这个赋值操作并不会改变 stack 的类型和它内部元素的值。赋值结束后,floatStack 中的元素仍旧是 float 类型,因此 top() 返回的也是 float 类型的元素。

看起来这个函数在你为任何类型的 stack 赋值时会关闭类型检查,但是事实上并非如此。必要的类型检查会在元素插入到目标 stack 时进行:

1
elems.push_front(tmp.top());

例如,存储 string 类型的 stack 给存储 float 类型的 stack 赋值时,这行语句的编译会出现错误信息:tmp.top() 返回的 string 类型不能作为 elems.push_front() 的参数(这条信息在不同编译器上不同,但意思基本如此):

1
2
3
4
Stack<std::string> stringStack; // stack of strings
Stack<float> floatStack; // stack of floats
...
floatStack = stringStack; // ERROR: std::string doesn't convert to float

同样,你可以改变对内部的容器类型进行参数化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<typename T, typename Cont = std::deque<T>>
class Stack
{
private:
Cont 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, typename Cont2>
Stack& operator= (Stack<T2,Cont2> const&);
// to get access to private members of Stack<T2> for any type T2:
template<typename, typename> friend class Stack;
};

模板赋值运算符的实现像下面这样:

1
2
3
4
5
6
7
8
9
10
11
template<typename T, typename Cont>
template<typename T2, typename Cont2>
Stack<T,Cont>&
Stack<T,Cont>::operator= (Stack<T2,Cont2> const& op2)
{
elems.clear(); // remove existing elements
elems.insert(elems.begin(), // insert at the beginning
op2.elems.begin(), // all elements from op2
op2.elems.end());
return *this;
}

记住,对于类模板来说,只有那些被调用的成员函数才会被实例化。因此,如果你禁止了不同类型的 stack 之间的赋值操作,你甚至可以使用 vector 作为内部的容器类型:

1
2
3
4
5
6
// stack for ints using a vector as an internal container
Stack<int, std::vector<int>> vStack;
...
vStack.push(42);
vStack.push(7);
std::cout << vStack.top() << '\n';

因为赋值运算符模板不是必要的,所以这个程序是正确的也不会有缺失成员函数 push_front() 的错误信息。

特例化成员函数模板

成员函数模板也可以被偏特化或全特化。例如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// boolstring.hpp
class BoolString
{
private:
std::string value;
public:
BoolString (std::string const& s)
: value(s) {}
template<typename T = std::string>
T get() const // get value (converted to T)
{
return value;
}
};
1
2
3
4
5
6
// full specialization for BoolString::getValue<>() for bool
template<>
inline bool BoolString::get<bool>() const
{
return value == "true" || value == "1" || value == "on";
}

注意你没有必要而且也不能声明这些特例化版本,你只能定义他们。因为这是一个在头文件的全特化版本,所以你必须将它声明成 inline 的去避免在不同翻译单元内重复定义的错误。

你可像这样使用类的全特化版本:

1
2
3
4
5
6
std::cout << std::boolalpha;
BoolString s1("hello");
std::cout << s1.get() << '\n'; // hello
std::cout << s1.get<bool>() << '\n'; // prints false
BoolString s2("on");
std::cout << s2.get<bool>() << '\n'; // prints true

特殊成员函数模板

成员函数模板可以用在任何可以拷贝或者移动的对象的特殊成员函数。类似于前面定义的赋值运算符模板,构造函数也可以定义成模板。但是,注意构造函数模板或是赋值运算符模板不能替代原先定义的构造函数和赋值运算符。成员函数模板不会被算为可以拷贝或移动对象的特殊成员函数。在这个例子中,对于为同一类型的 stack 赋值,仍然调用默认的赋值运算符。

这个功能既有好处也有坏处:

  • 可能会发生模板构造函数或赋值运算符比原先定义的拷贝/移动构造函数或赋值运算符更加匹配,尽管使用模板的版本只提供给其他的类型。6.2 节(未写)会详细介绍。
  • 将拷贝/移动构造函数模板化并不简单,例如约束它们存在的场景。6.4(未写)会详细介绍。

5.5.1 构造.template

有时,调用成员模板时显式的表明模板参数是很有必要的。在这种情况下,你必须使用 template 关键字来确保 < 解析为模板参数列表开头的标识。考虑如下的使用标准库中的 bitset 类型的例子:

1
2
3
4
5
6
template<unsigned long N>
void printBitset(std::bitset<N> const& bs)
{
std::cout << bs.template to_string<char, std::char_traits<char>,
std::allocator<char>>();
}

对于 bitset 类型的 bs 我们调用了它的成员函数模板 to_string(),并显式地指出了 string 类型的模板参数。如果没有使用额外的 .template,编译器不会知道 < 符号其实不是小于运算符而是模板参数列表开头的标识。这一问题只在点号之前对象的构造是依赖于模板参数时出现。在我们这个例子中,参数bs 依赖于模板参数 N

.template 符号(类似的符号有 ->template::template)仅能用于模板的内部,并且它前面的对象依赖于模板参数。13.3.3 节(未写) 会详细介绍。

5.5.2 泛型 Lambda 与成员模板

C++14 引入了泛型 lambda,它是成员模板的一种简写。一个简单的计算两个不同类型参数的和的 lambda 如下:

1
2
3
4
[](auto x, auto y)
{
return x + y;
}

它是下面这个类的一个默认构造对象的一个简写:

1
2
3
4
5
6
7
8
9
10
class SomeComplierSpeciaficName
{
public:
SomeComplierSpeciaficName();
template<typename T1, typename T2>
auto operator() (T1 x, T2 y) const
{
return x + y;
}
};

15.10.6 节(未写)会详细介绍。

5.6 变量模板

C++14 开始,变量也可以被特定的类型参数化。这被称为变量模板。

例如,你可以使用如下的代码来定义 $\pi$ 的值但不定义它的类型:

1
2
template<typename T>
constexpr T pi{3.1415926535897932385};

注意,对于所有的模板,它的定义可能不能出现在函数内或者是块作用域内部。

为了使用变量模板,你必须指明它的类型。例如,下方的代码在 pi<> 定义的作用域内使用了两种不同的变量:

1
2
std::cout << pi<double> << '\n';
std::cout << pi<float> << '\n';

你也可以声明使用不同的编译单元的变量模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//======= header.hpp
template<typename T> T val{}; // zero initialized value

//======= translation unit 1:
#include "header.hpp"

int main()
{
val<long> = 42;
print();
}

//======= translation unit 2:
#include "header.hpp"

void print()
{
std::cout << val<long> << '\n'; // OK: prints 42
}

变量办也可以有默认的模板参数:

1
2
template <typename T = long double>
constexpr T pi = {3.1415926535897932385};

你可以使用默认的后者是其他的类型:

1
2
std::cout << pi<> << '\n';        // outputs a long double
std::cout << pi<float> << '\n'; // outputs a float

但是,注意你必须使用尖括号,如果直接使用 pi 会产生错误:

1
std::cout << pi << '\n'; // ERROR

变量模板也可以被非类型参数实例化参数化,这也用于对初始化器的参数化。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <array>

template<int N>
std::array<int, N> arr{}; // array with N elements, zero-initialized
template<auto N>
constexpr decltype(N) dval = N; // type of dval depends on passed value

int main()
{
std::cout << dval<'c'> << '\n'; // N has value 'c' of type char
arr<10>[0] = 42; // set first element of global arr
for (std::size_t i = 0; i < arr<10>.size(); ++i) // uses values set in arr
{
std::cout << arr<10>[i] << '\n';
}
}

注意,甚至 arr 的初始化和迭代发生在不同的编译单元,使用了还是全局作用域的 std::array<int, 10> arr 变量。

数据成员的变量模板

一个实用变量模板的应用是定义一个代表类模板成员的变量。例如,这个类模板是如下定义的:

1
2
3
4
5
6
template<typename T>
class MyClass
{
public:
static constexpr int max = 100;
};

它允许你为不同的 MyClass<> 特例化版本定义不同的值,所以你可以定义:

1
2
template<typename T>
int myMax = MyClass<T>::max;

想使用时可以直接写

1
auto i = myMax<std::string>

而不是

1
auto i = MyClass<std::string>::max;

这意味着,对于标准库中的类:

1
2
3
4
5
6
7
8
9
10
namespace std
{
template<typename T> class nueric_limits
{
public:
...
static constexpr bool is_signed = false;
...
};
}

你可以定义

1
2
template<typename T>
constexpr bool isSigned = std::numeric_limits<T>::is_signed;

然后你就可以写这段代码

1
isSigned<char>

而不是

1
std::numeric_limits<char>::is_signed

Type Traits Suffix_v

从 C++17 开始,标准库使用变量模板的技术去对所有返回一个值(Boolean)的 type traits 定义了简写。例如,为了能够写

1
std::is_const_v<T> // since C++17

而不是

1
std::is_const<T>::value // since C++11

标准库定义了

1
2
3
4
namespace std
{
template<typename T> constexpr bool is_const_v = is_const<T>::value;
}

5.7 模板模板参数

模板参数本身作为类模板是非常有用的。同样,我们的 Stack 类模板可以作为一个例子。

为了对 stack 使用不同的内部容器,程序员需要显式地指出元素类型两次。因此,为了特化内部容器的类型,你必须传递容器的类型和它内部元素的类型:

1
Stack<int, std::vector<int>> vStack; //integer Stack that uses a vector

通过使用模板模板参数,你可以声明一个 Stack 类模板,只特例化其容器的类型而不用再特例化容器中元素的类型:

1
Stack<int, std::vector> vStack; //intege stack that uses a vector

为了完成这个,你必须特化第二个模板参数作为模板模板参数。原则上,它看起来如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T,
template <typename 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();
}
//...
};

不同的是,第二个模板参数被定义为类模板:

1
template<typename Elem> class Cont

它的默认值从 std::deque<T> 变为 std::deque。这个参数一定是一个模板参数,它被第一个模板参数传递的类型初始化:

1
Cont<T> elems;

这种使用第一个模板参数去初始化第二个模板参数在这个例子中很特殊。通常,你可以用任何一个类模板中的类型初始化一个模板模板参数。

通常,对于模板参数,除了可以使用 typename 你也可以使用 class 关键字。在 C++11 之前,Cont 只能被替代为类模板的名称。

1
2
3
4
5
6
template<typename T,
template<class Elem> class Cont = std::deque>
class Stack // OK
{
...
};

从 C++11 开始,我们也可以将 Cont 替代为一个别名模板,但是知道 C++17 才进行了相应的更改,允许使用关键字 typename 而不是 class 来声明模板模板参数:

1
2
3
4
5
6
template<typename T,
template<typename Elem> typename Cont = std::deque>
class Stack // ERROR before C++17
{
...
};

这两个变体的意义相同:使用 class 而不是 typename 并不会阻止我们指定一个别名模板,作为 Cont 参数相对应的参数。

因为模板模板参数的模板参数没有被使用,我们习惯上省略它的名字(除非他提供了有用的文档):

1
2
3
4
5
6
template<typename T,
template<typename> class Cont = std::deque>
class Stack
{
...
};

成员函数必须做相应的改变。因此,你必须特化第二个模板参数作为模板模板参数。成员函数也是如此。例如成员函数 push(),它的实现如下:

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

注意当模板模板参数是类或别名模板的占位符是,那么就没有函数或变量模板的占位符。

模板模板参数匹配

如果你尝试使用 Stack 的新的版本,你可能会获得一个错误信息:std::deque 的默认值与模板模板参数 Cont 不兼容。这是因为在 C++17 之前,模板模板参数必须是一个模板,它的参数和替代它的模板模板参数完全匹配,除了可变模板参数相关的例外(12.3.4 节(未写)会详细介绍)。模板模板参数的默认模板参数没有被考虑,因此通过省略具有默认值的参数无法实现匹配(C++17 中,默认模板参数已经被考虑到了)。

在 C++17 之前,这个例子的问题是 std::deque 标准库模板有一个以上的参数:第二个参数 allocator 拥有一个默认值,但是 C++17 之前并没有考虑将 std::dequeCont 参数匹配。

作为变通,我们可以重写类的声明从而使 Cont 参数期望有两个模板:

1
2
3
4
5
6
7
8
9
10
template<typename T,
template<typename Elem,
typename Alloc= std::allocator<Elem>>
class Cont = std::deque>
class Stack
{
private:
Cont<T> elems; // elements
...
};

同样我们可以省略 Alloc 因为它没有被使用。

我们最终的 Stack 类模板(包含对不同元素类型的stack进行赋值运算的成员模板)如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#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; // elemenats

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 ofatype 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 copy of 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(), // insert at the beginning
op2.elems.begin(), // all elements from op2
op2.elems.end());
return *this;
}

同样,为了访问所有 op2 成员,我们为 stack 的所有实例定义了友元(忽略模板参数的名称):

1
2
template<typename, template<typename, typename>class>
friend class Stack;

然而,不是所有标准库模板容器都可以作为 Cont 参数。例如,std::array 就不行,因为它包含了一个代表数组长度的非类型模板参数,所以它不会和这个模板模板参数匹配。

下面的程序使用了最后版本的所有 feature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "stack9.hpp"
#include <iostream>
#include <vector>

int main()
{
Stack<int> iStack; // stack of ints
Stack<float> fStack; // stack 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 doubless 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';
}

这个程序有以下输出:

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

对模板模板参数的更多介绍和例子,参见 12.2.3 节(未写)12.3.4 节(未写)19.2.2 节(未写)

5.8 小结

  • 为了使用依赖于模板参数的类型,你必须在它的名称前加上 typename 关键字修饰。
  • 为了访问依赖于模板参数的基类中的成员,你必须用 this-> 或者类名修饰。
  • 嵌套类和成员函数也可以是模板。一种应用场景是实现内部的类型转换的泛型代码。
  • 构造函数模板和赋值运算模板不会替换前面定义的构造函数,和赋值运算函数。
  • 通过使用大括号初始化列表或者显示的调用默认构造函数,你可以保证变量和成员模板会被初始化为默认值,即使他们被实例化为 built-in 类型。
  • 你可以为原始数组提供一个特殊的模板,它也可以用于字符串常量。
  • 当传递原始数组和字符串常量时,且参数不是按引用传递的,参数会退化(decay)(表现为数组变为指针)。
  • 你可以定义变量模板(C++14 以后)。
  • 你也可以使用类模板作为模板参数,这成为模板模板参数。
  • 模板模板参数必须和它们的参数准确匹配。