关键字typename

C++在标准化过程中,引入了关键字typename来说明模板内部的某个标识符是一种类型:

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
#include <iostream>

class B
{
public:
using Subtype = int;
};

template <typename T>
class A
{
public:
typename T::Subtype *ptr;
};

int main()
{
A<B> a;
a.ptr = new int(1);
std::cout << *a.ptr << std::endl;
delete a.ptr;

return 0;
}

T::Subtype是定义在T类中的一个类型,这里需要使用typename关键字来特殊注明它是一种类型,若不使用typename关键字,那么编译器会优先匹配T类的非类型成员,如静态数据成员和枚举常量。虽然不一定会出错,但是为了增强代码的可读性和防止可能的错误,只要依赖模板参数的名称是一种类型,就必须使用typename

这种声明广泛应用于STL中,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
void print(const T &container)
{
typename T::const_iterator it = container.begin();
typename T::const_iterator end = container.end();
for (; it != end; ++it)
{
std::cout << *it << ' ';
}
}
...
std::vector<int> v = {1, 2, 3, 4, 5};
print(v);

print()函数可接收一种STL容器作为参数,如示例所示,Tstd::vector<int>类型,那么itend就可使用typename T::const_iterator来获取指向容器首尾的只读迭代器。

C++20后,可能存在许多常见情况下,将不再需要typename

零初始化

对于基础内置类型(如int,double)并不存在可用的默认初始化函数,因此它们的未初始化的局部变量的值将是不确定的。

现在,让我们新建一个类:

1
2
3
4
5
6
template <typename T>
void foo()
{
T x;
// Code
}

如果T为简单类型,我们发现并不存在有效的令x初始化的方法。

出于这个原因,我们可以显示调用内置类型的默认构造函数,将它们初始化为0。

类型 默认初始化值
数字类型int,double,long long… 0
bool false
pointer nullptr

使用方法

1
2
T x{};
// 或 T x = T{};

这种初始化方式称为值初始化,这意味着要么调用本身提供的构造函数,要么用0初始化对象。即时构造函数声明为显式,也可以这么做。

1
2
3
4
5
6
7
8
9
template <typename T>
class Base
{
public:
T data;
explicit Base() : data{} {}
};
...
Base<int> b{};

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

1
T x = T();

在C++17前,只有选择用于拷贝构造函数为非显式时,才能这么做。C++17中的强制拷贝省略解除了这一限制,因而两种语法都可以使用。

从C++11开始,非静态的类成员也可以使用默认初始化:

1
2
3
4
5
6
template <typename T>
class Base
{
public:
T data{};
};

但是默认实参不能使用该语法,必须显式表示出来:

1
2
3
4
5
6
7
8
9
template <typename T>
void foo(T p{}) { // error

}

template <typename T>
void foo(T p = T{}) { // ok

}

使用this->

对于基类依赖于模板参数的类模板,我们使用基类的成员时,必须使用this->

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

当我们创建一个Derived类实例并且调用foo()函数时,程序报错:error: there are no arguments to 'bar' that depend on a template parameter, so a declaration of 'bar' must be available [-fpermissive]。编译器不会考虑基类中的bar(),而是会选择去外部寻找。我们会在之后详细探讨这个问题的成因,在此之前,要记住基类是模板时,使用this->或者Base<T>::

处理原始数组和字符串字面量的模板

在我们将原始数组或字符串字面量传递给模板时,一定要注意模板参数是声明为值传递还是引用传递。假如传递int a[]数组,若为引用传递,那么推到类型T不会退化,为int const[];而若为值传递,那么T会退化为int const*

我们还可以定义专门处理原始数组和字符串字面量的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

自定义了一个专门处理原始数组和字符串字面量的less<>()函数,用于判断数组或者字符串的字典序大小。