成员模板

对于嵌套类和成员函数来说,它们也有可能是模板,下面让我们用C++模板入门(二)类模板中的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
template <typename T>
class Stack {
public:
Stack() = default;
~Stack() = default;

void push(const T& value) {
stack.push_back(value);
}

void pop() {
assert(!stack.empty());
stack.pop_back();
}

const T& top() const {
assert(!stack.empty());
return stack.back();
}

bool empty() const {
return stack.empty();
}

size_t size() const {
return stack.size();
}
private:
std::vector<T> stack;
};

如果我们不使用模板类,想要给它定义一个拷贝构造函数或者赋值函数,那么两个Stack<>类的类型必须相同,即时它们的类型之间可以发生隐式转换,由于内部使用的是std::vector<>,所以隐式拷贝构造函数即可实现功能:

1
2
Stack(const Stack &stack) = default;
Stack &operator=(const Stack &stack) = default;

问题也正如我们所说,如果我们想将一个Stack<double>类型的栈拷贝构造或者赋值给当前指定为Stack<int>类型的栈对象时,会出现错误。这种问题我们之前也提到过,模板支持简单的类型退化操作,却并不支持隐式的类型转换。

这时候我们就可以将拷贝构造函数或者赋值函数声明为函数模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename U>
Stack(const Stack<U> &stack)
{
for (const U &value : stack.stack)
{
push(value);
}
}
template <typename U>
Stack &operator=(const Stack<U> &stack)
{
if (this != &stack)
{
stack.clear();
for (const U &value : stack.stack)
{
push(value);
}
}
return *this;
}

这样,在无法匹配与当前对象类型相同的拷贝构造或者赋值语句时,就会调用这两个函数模板,进行相应的操作。当我们尝试使用这两个函数时,如:

1
2
3
4
5
Stack<int> example4;
example4.push(1);
example4.push(2);
example4.push(3);
Stack<double> example5 = example4;

程序出现了报错error: 'std::vector<int, std::allocator<int> > Stack<int>::stack' is private within this contex for (const U &value : 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
std::vector<T>::const_iterator begin() const
{
return stack.begin();
}
std::vector<T>::const_iterator end() const
{
return stack.end();
}
template <typename U>
Stack(const Stack<U> &stack)
{
typename std::vector<U>::const_iterator it = stack.begin();
typename std::vector<U>::const_iterator end = stack.end();
for (; it != end; ++it)
{
push(*it);
}
}
template <typename U>
Stack &operator=(const Stack<U> &stack)
{
if (this != &stack)
{
stack.clear();
typename std::vector<U>::const_iterator it = stack.begin();
typename std::vector<U>::const_iterator end = stack.end();
for (; it != end; ++it)
{
push(*it);
}
}
return *this;
}

我们提供了获取指向栈顶和栈底的const_iterator(事实上就是使用的std::vector<>的迭代器)的公有方法,来帮助我们实现功能。注意这种转换只是弥补了可以进行类型转换的数据类型间的拷贝构造和赋值等问题,如果你想将一个std::string类型的栈赋值给int类型的栈,毫无疑问它将在实例化的函数模板内部报错。

.template构造

有时,在调用成员模板时,需要显式限定模板实参。在这种类型下,必须使用template关键字来确保符号<会解析为模板实参列表的起始符:

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

对于bitset,调用了其内部成员函数模板to_string(),同时显式指定字符串类型的详细信息(to_string函数后面的所有模板参数信息)。如果不额外使用.template,编译器就会认为to_string后面的<是小于号。注意只有点号.之前的构造对象依赖模板参数时,才会这样使用,如这个例子中的bitset依赖于参数unsigned int N

.template(以及和它作用相似的->template::template)应该只在模板内部使用,并且只有当位于它们之前的对象依赖模板参数时才使用。

泛型lambda和成员模板

C++14新增了如下的lambda表达式:

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

它接收两个任意参数的x和y,并返回它们的和,下面是它内部的一种实现形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class __lambda_6_13
{
public:
template <class type_parameter_0_0, class type_parameter_0_1>
inline /*constexpr */ auto operator()(type_parameter_0_0 x, type_parameter_0_1 y) const
{
return x + y;
}

private:
template <class type_parameter_0_0, class type_parameter_0_1>
static inline /*constexpr */ auto __invoke(type_parameter_0_0 x, type_parameter_0_1 y)
{
return __lambda_6_13{}.operator()<type_parameter_0_0, type_parameter_0_1>(x, y);
}

public:
// /*constexpr */ __lambda_6_13() = default;
};

__lambda_6_13 f = __lambda_6_13{};

可以看到,这里的泛型lambda表达式其实就是使用了函数模板。

变量模板

从C++14开始,变量也可以通过特定类型来参数化,称之为变量模板:

1
2
3
4
5
template <typename T>
constexpr T pi = T(3.1415926535897932385L);
std::cout << pi<double> << '\n';
std::cout << pi<float> << '\n';
std::cout << pi<int> << '\n';

数据成员的变量模板

变量模板的一个应用就是定义代表类模板成员的变量,如:

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

我们可以对MyClass类进行特化,从而获得不同的max值。对于MyClass类的不同实例化版本,我们可以为它们定义:

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

那么我们在使用时就可以直接使用mymax<T>而非MyClass<T>::max,这就有了我们如下标准库中的:

类型特征后缀_v

自C++17后,标准库使用了变量模板对标准库中所有的萃取所得为bool值的类型特征定义了快捷表达式,如:

1
std::is_pointer<T>::val

可以写作:

1
std::is_pointerv<T>

标准库中的定义:

1
2
template <typename _Tp>
inline constexpr bool is_pointer_v = is_pointer<_Tp>::value;

模板的模板参数简介

我们可以让模板的参数也是一个模板,比如我们想自定义Stack<>类的存储方式,就可以指定为它传入一个模板容器,如果不使用模板参数,我们需要标明两次元素类型,在模板类声明中:

1
2
template <typename T, typename Container = std::vector<T>>
class Stack;

注:声明模板类时若注明默认参数,那么再定义模板类时不需要再次指明默认参数。

使用:

1
Stack<int, std::vector<int>> stack;

有没有方法能够只声明一次类型呢,看下面一个例子,来实例一个使用std::vector<int>存储的栈,如果我们使用模板参数,那么便不需要声明两次类型:

1
Stack<std::vector<int>> stack;

我们在参数中也使用了一个模板类,其定义需要如下实现形式:

1
2
template <template <typename T> typename Container>
class Stack;

这样,我们就可以向模板参数中再传递一个模板。