第一章 函数模版
1.1 初识模版
1.1.1定义模版
以下就是一个函数模板,它返回两个数之中的最大值:
template<typename T>
T max (T a, T b)
{
// 如果 b < a, 返回 a,否则返回 b
return b < a ? a : b;
}
模板参数是 typename T。请留意<和>的使用,它们在这里被称为尖括号。
关键字 typename 标识了一个类型参数。这是到目前为止 C++中模板参数最典型的用法,当
然也有其他参数(非类型模板参数),我们将在第 3 章介绍。
在这里 T 是类型参数。你可以用任意标识作为类型参数名,但是习惯上是用 T。类型参数可
以代表任意类型,它在模板被调用的时候决定。但是该类型(可以是基础类型,类或者其它
类型)应该支持模板中用到的运算符
关键字 typename 标识了一个类型参数。这是到目前为止 C++中模板参数最典型的用法,当
然也有其他参数(非类型模板参数),我们将在第 3 章介绍。
在这里 T 是类型参数。你可以用任意标识作为类型参数名,但是习惯上是用 T。类型参数可
以代表任意类型,它在模板被调用的时候决定。但是该类型(可以是基础类型,类或者其它
类型)应该支持模板中用到的运算符
1.1.3 两阶段编译检查(Two-Phase Translation )
在实例化模板的时候,如果模板参数类型不支持所有模板中用到的操作符,将会遇到编译期
错误。模板是被分两步编译的:
1.
在模板定义阶段,模板的检查并不包含类型参数的检查。只包含下面几个方面:
- 语法检查。比如少了分号。
- 使用了未定义的不依赖于模板参数的名称(类型名,函数名,......)。
- 未使用模板参数的 static assertions。
2. 在模板实例化阶段,为确保所有代码都是有效的,模板会再次被检查,尤其是那些依赖于类型参数的部分
template<typename T>
void foo(T t)
{
undeclared(); // 如果 undeclared()未定义,第一阶段就会报错,因为与模板参
数无关
undeclared(t); //如果 undeclared(t)未定义,第二阶段会报错,因为与模板参
数有关
static_assert(sizeof(int) > 10,"int too small"); // 与模板参数无关,
总是报错
static_assert(sizeof(T) > 10, "T too small"); //与模板参数有关,只会
在第二阶段报错
}
1.2 模板参数推断
当我们调用形如 max()
的函数模板来处理某些变量时,模板参数将由被传递的调用参数决定。
如果我们传递两个
int
类型的参数给模板函数,
C++
编译器会将模板参数
T
推断为
int
。
不过
T
可能只是实际传递的函数参数类型的一部分。比如我们定义了如下接受常量引用作为
函数参数的模板:
template<typename T>
T max (T const& a, T const& b)
{
return b < a ? a : b;
}
此时如果我们传递 int
类型的调用参数,由于调用参数和
int const &
匹配,类型参数
T 将被推断为
int
类型推断中的类型转换
在类型推断的时候自动的类型转换是受限制的:
- 如果调用参数是按引用传递的,任何类型转换都不被允许。通过模板类型参数 T 定义的 两个参数,它们实参的类型必须完全一样。
- 如果调用参数是按值传递的,那么只有退化(decay)这一类简单转换是被允许的:const 和 volatile 限制符会被忽略,引用被转换成被引用的类型,raw array 和函数被转换为相 应的指针类型。通过模板类型参数 T 定义的两个参数,它们实参的类型在退化(decay) 后必须一样。
例如:
template<typename T>
T max (T a, T b); …
int const c = 42;
Int i = 1; //原书缺少 i 的定义
max(i, c); // OK: T 被推断为 int,c 中的 const 被 decay 掉
max(c, c); // OK: T 被推断为 int
int& ir = i;
max(i, ir); // OK: T 被推断为 int, ir 中的引用被 decay 掉
int arr[4];
foo(&i, arr); // OK: T 被推断为 int*
但是像下面这样是错误的:
max(4, 7.2); // ERROR: 不确定 T 该被推断为 int 还是 double
std::string s;
foo("hello", s); //ERROR: 不确定 T 该被推断为 const[6] 还是 std::string
有两种办法解决以上错误:
1. 对参数做类型转换
max
(
static_cast
<
double
>(
4
),
7.2
);
// OK
2. 显式地指出类型参数
T
的类型,这样编译器就不再会去做类型推导。
max
<
double
>(
4
,
7.2
);
// OK
3. 指明调用参数可能有不同的类型(多个模板参数)。
对默认调用参数的类型推断
需要注意的是,类型推断并不适用于默认调用参数。例如:
template
<
typename
T
>
void
f
(
T
=
""
);
...
f
(
1
);
// OK: T
被推断为
int,
调用
f<int> (1)
f
();
// ERROR:
无法推断
T
的类型
为应对这一情况,你需要给模板类型参数也声明一个默认参数,
1.4
节会介绍这一内容:
template
<
typename
T
=
std
::
string
>
void
f
(
T
=
""
);
…
f
();
// OK
1.3 多个模板参数
目前我们看到了与函数模板相关的两组参数:
1.
模板参数,定义在函数模板前面的尖括号里:
template
<
typename
T
>
// T
是模板参数
2.
调用参数,定义在函数模板名称后面的圆括号里:
T max
(
T a
,
T b
)
// a
和
b
是调用参数
模板参数可以是一个或者多个。比如,你可以定义这样一个 max()
模板,它可能接受两个不
同类型的调用参数:
template<typename T1, typename T2>
T1 max (T1 a, T2 b)
{
return b < a ? a : b;
}…
auto m = ::max(4, 7.2); // OK, 但是返回类型是第一个模板参数 T1 的类型
看上去如你所愿,它可以接受两个不同类型的调用参数。但是如示例代码所示,这也导致了
一个问题。如果你使用其中一个类型参数的类型作为返回类型,不管是不是和调用者预期地
一样,当应该返回另一个类型的值的时候,返回值会被做类型转换。这将导致返回值的具体
类型和参数的传递顺序有关。如果传递
66.66
和
42
给这个函数模板,返回值是
double
类型
的
66.66
,但是如果传递
42
和
66.66
,返回值却是
int
类型的
66
。
C++
提供了多种应对这一问题的方法:
- 1. 引入第三个模板参数作为返回类型。
- 2. 让编译器找出返回类型。
- 3. 将返回类型定义为两个参数类型的“公共类型”
下面将逐一进行讨论。
1.3.1 作为返回类型的模板参数
按照之前的讨论,模板类型推断允许我们像调用普通函数一样调用函数模板:我们可以不去
显式的指出模板参数的类型。
但是也提到,我们也可以显式的指出模板参数的类型:
template<typename T>
T max (T a, T b);
…
::max<double>(4, 7.2); // max()被针对 double 实例化
当模板参数和调用参数之间没有必然的联系,且模板参数不能确定的时候,就要显式的指明
模板参数。比如你可以引入第三个模板来指定函数模板的返回类型:
template
<
typename
T1
,
typename
T2
,
typename
RT
>
RT max
(
T1 a
,
T2 b
);
但是模板类型推断不会考虑返回类型,而 RT
又没有被用作调用参数的类型。因此
RT
不会被
推断。这样就必须显式的指明模板参数的类型。比如:
template
<
typename
T1
,
typename
T2
,
typename
RT
>
RT max
(
T1 a
,
T2 b
);
…
::
max
<
int
,
double
,
double
>(
4
,
7.2
);
// OK,
但是太繁琐
到目前为止,我们看到的情况是,要么所有模板参数都被显式指定,要么一个都不指定。另
一种办法是只指定第一个模板参数的类型,其余参数的类型通过推断获得。通常而言,我们
必须显式指定所有模板参数的类型,直到某一个模板参数的类型可以被推断出来为止。因此,
如果你改变了上面例子中的模板参数顺序,调用时只需要指定返回值的类型就可以了:
template
<
typename
RT
,
typename
T1
,
typename
T2
>
RT max
(
T1 a
,
T2 b
);
…
::
max
<
double
>(
4
,
7.2
)
//OK:
返回类型是
double
,
T1
和
T2
根据调用参数推断
在本例中,调用 max<double>
时,显式的指明了
RT
的类型是
double
,
T1
和
T2
则基于传入
调用参数的类型被推断为
int
和
double
。
然而改进版的 max()
并没有带来显著的变化。使用单模板参数的版本,即使传入的两个调用
参数的类型不同,你依然可以显式的指定模板参数类型(也作为返回类型)。因此为了简洁,
我们最好还是使用单模板参数的版本。(在接下来讨论其它模板问题的时候,我们也会基于
单模板参数的版本)
1.3.2 返回类型推断
如果返回类型是由模板参数决定的,那么推断返回类型最简单也是最好的办法就是让编译器
来做这件事。从
C++14
开始,这成为可能,而且不需要把返回类型声明为任何模板参数类型
(不过你需要声明返回类型为
auto
):
template<typename T1, typename T2>
auto max (T1 a, T2 b)
{
return b < a ? a : b;
}
事实上,在不使用尾置返回类型(
trailing return type
)的情况下
将
auto
用于返回类型,要求
返回类型必须能够通过函数体中的返回语句推断出来。当然,这首先要求返回类型能够从函
数体中推断出来。因此,必须要有这样可以用来推断返回类型的返回语句,而且多个返回语
句之间的推断结果必须一致。
在 C++14
之前,要想让编译器推断出返回类型,就必须让或多或少的函数实现成为函数声明
的一部分。在
C++11
中,尾置返回类型(
trailing return type
)允许我们使用函数的调用参数。
也就是说,我们可以基于运算符
?:
的结果声明返回类型:
template<typename T1, typename T2>
auto max (T1 a, T2 b) -> decltype(b<a?a:b)
{
return b < a ? a : b;
}
在这里,返回类型是由运算符?:的结果决定的,这虽然复杂但是可以得到想要的结果。
需要注意的是
template<typename T1, typename T2>
auto max (T1 a, T2 b) -> decltype(b<a?a:b);
是一个声明,编译器在编译阶段会根据运算符?:的返回结果来决定实际的返回类型。不过具
体的实现可以有所不同,事实上用 true 作为运算符?:的条件就足够了:
template<typename T1, typename T2>
auto max (T1 a, T2 b) -> decltype(true?a:b);
但是在某些情况下会有一个严重的问题:由于 T
可能是引用类型,返回类型就也可能被推断
为引用类型。因此你应该返回的是
decay
后的
T
,像下面这样:
template<typename T1,typename T2>
auto max(T1 a, T2 b) -> typename std::decay<decltype(true ? a : b)>::type
{
return b < a ? a : b;
}
在这里我们用到了类型萃取(type trait
)
std::decay<>
,它返回其
type
成员作为目标类型,
定义在标准库
<type_trait>
中(参见
D.5
)。由于其
type
成员是一个类型,为了获取其结果,
需要用关键字
typename
修饰这个表达式。
在这里请注意,在初始化 auto
变量的时候其类型总是退化之后了的类型。当返回类型是
auto
的时候也是这样。用
auto
作为返回结果的效果就像下面这样,
a
的类型将被推断为
i
退化后
的类型,也就是
int
:
int
i
=
42
;
int const
&
ir
=
i
;
// ir
是
i
的引用
auto
a
=
ir
;
// a
的类型是
it decay
1.3.3 将返回类型声明为公共类型(Common Type)
从 C++11
开始,标准库提供了一种指定“更一般类型”的方式。
std::common_type<>::type
产生的类型是他的两个模板参数的公共类型。比如:
#include <type_traits>
template<typename T1, typename T2>
std::common_type_t<T1,T2> max (T1 a, T2 b)
{
return b < a ? a : b;
}
同样的,std::common_type
也是一个类型萃取(
type trait
),定义在
<type_traits>
中,它返
回一个结构体,结构体的
type
成员被用作目标类型。因此其主要应用场景如下:
typename
std
::
common_type
<
T1
,
T2
>::
type
//since C++11
不过从 C++14
开始,你可以简化“萃取”的用法,只要在后面加个
_t
,就可以省掉
typename
和
::type
(参见
2.8
节),简化后的版本变成:
std
::
common_type_t
<
T1
,
T2
>
// equivalent since C++14
std::common_type<>的实现用到了一些比较取巧的模板编程手法,具体请参见
25.5.2
节。它
根据运算符
?:
的语法规则或者对某些类型的特化来决定目标类型。因此
::max(4, 7.2)
和
::max(7.2, 4)
都返回
double
类型的
7.2.
需要注意的是,
std::common_type<>
的结果也是退
化的,具体参见
D.5
。
1.4 默认模板参数
你也可以给模板参数指定默认值。这些默认值被称为默认模板参数并且可以用于任意类型的
模板。它们甚至可以根据其前面的模板参数来决定自己的类型。
比如如果你想将前述定义返回类型的方法和多模板参数一起使用,你可以为返回类型引入一
个模板参数
RT
,并将其默认类型声明为其它两个模板参数的公共类型。同样地,我们也有
多种实现方法:
1.
我们可以直接使用运算符
?:
。不过由于我们必须在调用参数
a
和
b
被声明之前使用运算
符
?:
,我们只能像下面这样:
#include <type_traits>
template<typename T1, typename T2, typename RT =
std::decay_t<decltype(true ? T1() : T2())>>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}
请注意在这里我们用到了 std::decay_t<>
来确保返回的值不是引用类型。
同样值得注意的是,这一实现方式要求我们能够调用两个模板参数的默认构造参数。还有另
一种方法,使用
std::declval
,不过这将使得声明部分变得更加复杂。作为例子可以参见
11.2.3
节。
2.
我们也可以利用类型萃取
std::common_type<>
作为返回类型的默认值:
#include <type_traits>
template<typename T1, typename T2, typename RT =
std::common_type_t<T1,T2>>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}
在这里 std::common_type<>
也是会做类型退化的,因此返回类型不会是引用。
在以上两种情况下,作为调用者,你即可以使用
RT
的默认值作为返回类型:
auto
a
= ::
max
(
4
,
7.2
);
也可以显式的指出所有的模板参数的类型:
auto
b
= ::
max
<
double
,
int
,
long double
>(
7.2
,
4
);
但是,我们再次遇到这样一个问题:为了显式指出返回类型,我们必须显式的指出全部三个
模板参数的类型。因此我们希望能够将返回类型作为第一个模板参数,并且依然能够从其它两个模板参数推断出它的类型。
原则上这是可行的,即使后面的模板参数没有默认值,我们依然可以让第一个模板参数有默
认值:
template<typename RT = long, typename T1, typename T2>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}
基于这个定义,你可以这样调用:
int
i
;
long
l
;
…
max
(
i
,
l
);
//
返回值类型是
long (RT
的默认值
)
max
<
int
>(
4
,
42
);
//
返回
int
,因为其被显式指定
但是只有当模板参数具有一个“天生的”默认值时,这才有意义。我们真正想要的是从前面
的模板参数推导出想要的默认值。原则是这也是可行的,就如
26.5.1
节讨论的那样,但是他
是基于类型萃取的,并且会使问变得更加复杂。
基于以上原因,最好也是最简单的办法就是像 1.3.2
节讨论的那样让编译器来推断出返回类
型。
1.5 函数模板的重载
像普通函数一样,模板也是可以重载的。也就是说,你可以定义多个有相同函数名的函数,
当实际调用的时候,由
C++
编译器负责决定具体该调用哪一个函数。即使在不考虑模板的时
候,这一决策过程也可能异常复杂。本节将讨论包含模板的重载。如果你还不熟悉没有模板
时的重载规则,请先看一下附录
C
, 那里比较详细的总结了模板的解析过程。
下面几行程序展示了函数模板的重载:
// maximum of two int values:
int max (int a, int b)
{
return b < a ? a : b;
}
// maximum of two values of any type:
template<typename T>
T max (T a, T b)
{
return b < a ? a : b;
}
int main()
{
::max(7, 42); // calls the nontemplate for two ints
::max(7.0, 42.0); // calls max<double> (by argument deduction)
::max(’a’, ’b’); //calls max<char> (by argument deduction)
::max<>(7, 42); // calls max<int> (by argumentdeduction)
::max<double>(7, 42); // calls max<double> (no argumentdeduction)
::max(’a’, 42.7); //calls the nontemplate for two ints
}
如你所见,一个非模板函数可以和一个与其同名的函数模板共存,并且这个同名的函数模
板可以被实例化为与非模板函数具有相同类型的调用参数
。
在所有其它因素都相同的情况
下,模板解析过程将优先选择非模板函数,而不是从模板实例化出来的函数。
第一个调用
就属于这种情况:
::
max
(
7
,
42
);
// both int values match the nontemplate function perfectly
如果模板可以实例化出一个更匹配的函数,那么就会选择这个模板。正如第二和第三次调
用
max()
时那样:
::
max
(
7.0
,
42.0
);
// calls the max<double> (by argument deduction)
::
max
(
’
a
’
,
’
b
’
);
//calls the max<char> (by argument deduction)
在这里模板更匹配一些,因为它不需要从 double
和
char
到
int
的转换。(参见
C.2
中的模板
解析过程)
也可以显式指定一个空的模板列表。这表明它会被解析成一个模板调用,其所有的模板参
数会被通过调用参数推断出来:
::
max
<>(
7
,
42
);
// calls max<int> (by argument deduction)
由于在模板参数推断时不允许自动类型转换,而常规函数是允许的,因此最后一个调用会
选择非模板参函数(
‘a’
和
42.7
都被转换成
int
):
::
max
(
’
a
’
,
42.7
);
//only the nontemplate function allows nontrivial
conversions
一个有趣的例子是我们可以专门为 max()
实现一个可以显式指定返回值类型的模板:
template<typename T1, typename T2>
auto max (T1 a, T2 b)
{
return b < a ? a : b;
}
template<typename RT, typename T1, typename T2>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}
现在我们可以像下面这样调用
max():
auto
a
= ::
max
(
4
,
7.2
);
// uses first template
auto
b
= ::
max
<
long double
>(
7.2
,
4
);
// uses second template
但是像下面这样调用的话:
auto
c
= ::
max
<
int
>(
4
,
7.2
);
// ERROR: both function templates match
两个模板都是匹配的,这会导致模板解析过程不知道该调用哪一个模板,从而导致未知错误。
因此当重载函数模板的时候,你要保证对任意一个调用,都只会有一个模板匹配
1.6 难道,我们不应该...?
1.6.1 按值传递还是按引用传递?
你可能会比较困惑,为什么我们声明的函数通常都是按值传递,而不是按引用传递。通常而
言,建议将按引用传递用于除简单类型(比如基础类型和
std::string_view
)以外的类型,这
样可以免除不必要的拷贝成本。
不过出于以下原因,按值传递通常更好一些:
- 语法简单。
- 编译器能够更好地进行优化。
- 移动语义通常使拷贝成本比较低
- 某些情况下可能没有拷贝或者移动
再有就是,对于模板,还有一些特有情况:
- 模板既可以用于简单类型,也可以用于复杂类型,因此如果默认选择适合于复杂类型可 能方式,可能会对简单类型产生不利影响。
- 作为调用者,你通常可以使用 std::ref()和 std::cref()(参见 7.3 节)来按引用传递参数。
- 虽然按值传递 string literal 和 raw array 经常会遇到问题,但是按照引用传递它们通常只 会遇到更大的问题。第 7 章会对此做进一步讨论。在本书中,除了某些不得不用按引用 传递的地方,我们会尽量使用按值传递。
1.6.2 为什么不适用 inline?
通常而言,函数模板不需要被声明成 inline
。不同于非
inline
函数,我们可以把非
inline
的函
数模板定义在头文件里,然后在多个编译单元里
include
这个文件。
唯一一个例外是模板对某些类型的全特化,这时候最终的
code
不在是“泛型”的(所有的
模板参数都已被指定)。详情请参见
9.2
节。
严格地从语言角度来看,inline
只意味着在程序中函数的定义可以出现很多次
。不过它也给
了编译器一个暗示,在调用该函数的地方函数应该被展开成
inline
的:这样做在某些情况下
可以提高效率,但是在另一些情况下也可能降低效率。现代编译器在没有关键字
inline
暗示
的情况下,通常也可以很好的决定是否将函数展开成
inline
的。当然,编译器在做决定的时
候依然会将关键字
inline
纳入考虑因素。
1.6.3 为什么不用 constexpr?
从 C++11
开始,你可以通过使用关键字
constexpr
来在编译阶段进行某些计算。对于很多模
板,这是有意义的。
比如为了可以在编译阶段使用求最大值的函数,你必须将其定义成下面这样:
template<typename T1, typename T2>
constexpr auto max (T1 a, T2 b)
{
return b < a ? a : b;
}
如此你就可以在编译阶段的上下文中,实时地使用这个求最大值的函数模板:
int
a
[::
max
(
sizeof
(
char
),
1000u
)];
或者指定 std::array<>
的大小:
std
::
array
<
std
::
string
, ::
max
(
sizeof
(
char
),
1000u
)>
arr
;
在这里我们传递的 1000
是
unsigned int
类型,这样可以避免直接比较一个有符号数值和一个
无符号数值时产生的警报。
8.2 节还会讨论其它一些使用
constexpr
的例子。但是,为了更专注于模板的基本原理,我们
接下来在讨论模板特性的时候会跳过
constexpr
。
1.7 总结
- 函数模板定义了一组适用于不同类型的函数。
- 当向模板函数传递变量时,函数模板会自行推断模板参数的类型,来决定去实例化出那 种类型的函数。
- 你也可以显式的指出模板参数的类型。
- 你可以定义模板参数的默认值。这个默认值可以使用该模板参数前面的模板参数的类 型,而且其后面的模板参数可以没有默认值。
- 函数模板可以被重载。
- 当定义新的函数模板来重载已有的函数模板时,必须要确保在任何调用情况下都只有一 个模板是最匹配的。
- 当你重载函数模板的时候,最好只是显式地指出了模板参数得了类型。
- 确保在调用某个函数模板之前,编译器已经看到了相对应的模板定义。
第二章 类模版
2.1-2.2 实现与使用
// testtemplate.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <vector>
#include <cassert>
template<typename T>
class Stack {
private:
std::vector<T> elems; // elements
public:
void push(T const& elem); // 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();
}
};
template<typename T>
void Stack<T>::push(T const& elem)
{
elems.push_back(elem); // append copy of passed elem
}
template<typename T>
void Stack<T>::pop()
{
assert(!elems.empty());
elems.pop_back(); // remove last element
}
template<typename T>
T const& Stack<T>::top() const
{
assert(!elems.empty());
return elems.back(); // return copy of last element
}
int main()
{
Stack< int> intStack; // stack of ints
Stack<std::string> stringStack; // stack of strings
// manipulate int stack
intStack.push(7);
std::cout << intStack.top() << std::ends;
// manipulate string stack
stringStack.push("hello");
std::cout << stringStack.top() << std::ends;
stringStack.pop();
return 0;
}
注意,模板函数和模板成员函数只有在被调用的时候才会实例化。这样一方面会节省时间和
空间,同样也允许只是部分的使用类模板,我们会在
2.3
节对此进行讨论。
在这个例子中,对 int
和
std::string
,默认构造函数,
push()
以及
top()
函数都会被实例化。而
pop()
只会针对
std::string
实例化。如果一个类模板有
static
成员,对每一个用到这个类模板
的类型,相应的静态成员也只会被实例化一次。
2.3 部分地使用类模板
一个类模板通常会对用来实例化它的类型进行多种操作(包含构造函数和析构函数)。这可
能会让你以为,要为模板参数提供所有被模板成员函数用到的操作。但是事实不是这样:模
板参数只需要提供那些会被用到的操作(而不是可能会被用到的操作)。
比如 Stack<>
类可能会提供一个成员函数
printOn()
来打印整个
stack
的内容,它会调用
operator <<
来依次打印每一个元素:
template<typename T>
class Stack { …
void printOn() (std::ostream& strm) const {
for (T const& elem : elems) {
strm << elem << ’ ’; // call << for each element
}
}
};
这个类依然可以用于那些没有提供 operator <<
运算符的元素:
Stack
<
std
::
pair
<
int
,
int
>>
ps
;
// note: std::pair<> has no operator<< defined
ps
.
push
({
4
,
5
});
// OK
ps
.
push
({
6
,
7
});
// OK
std
::
cout
<<
ps
.
top
().
first
<<
’
\n
’
;
// OK
std
::
cout
<<
ps
.
top
().
second
<<
’
\n
’
;
// OK
只有在调用 printOn()
的时候,才会导致错误,因为它无法为这一类型实例化出对
operator<<
的调用:
ps
.
printOn
(
std
::
cout
);
// ERROR: operator<< not supported for element type
2.3.1 Concept(最好不要汉化这一概念)
这样就有一个问题:我们如何才能知道为了实例化一个模板需要哪些操作?名词 concept
通
常被用来表示一组反复被模板库要求的限制条件。例如
C++
标准库是基于这样一些
concepts
的:可随机进入的迭代器(
random access iterator
)和可默认构造的(
default constructible
)。
目前(比如在
C++17
中),
concepts
还只是或多或少的出现在文档当中(比如代码注释)。
这会导致严重的问题,因为不遵守这些限制会导致让人难以理解的错误信息(参考
9.4
节)。
近年来有一些方法和尝试,试图在语言特性层面支持对
concepts
的定义和检查。但是直到
C++17
,还没有哪一种方法得以被标准化。
从 C++11
开始,你至少可以通过关键字
static_assert
和其它一些预定义的类型萃取(
type
traits
)来做一些简单的检查。比如:
template<typename T>
class C
{
static_assert(std::is_default_constructible<T>::value,
"Class C requires default-constructible elements");
…
};
即使没有这个 static_assert
,如果需要
T
的默认构造函数的话,依然会遇到编译错误。只不
过这个错误信息可能会包含整个模板实例化过程中所有的历史信息,从实例化被触发的地方
直到模板定义中引发错误的地方(参见
9.4
节)。
然而还有更复杂的情况需要检查,比如模板类型 T
的实例需要提供一个特殊的成员函数,或
者需要能够通过
operator <
进行比较。这一类情况的详细例子请参见
19.6.3
节。
关于
C++ concept
的详细讨论,请参见附录
E
2.4 友元
相比于通过 printOn()
来打印
stack
的内容,更好的办法是去重载
stack
的
operator <<
运算符。
而且和非模板类的情况一样,
operator<<
应该被实现为非成员函数,在其实现中可以调用
printOn()
:
可以隐式的声明一个新的函数模板,但是必须使用一个不同于类模板的模板参数,比如
用
U
:
template
<
typename
T
>
class
Stack
{
…
template
<
typename
U
>
friend
std
::
ostream
&
operator
<< (
std
::
ostream
&,
Stack
<
U
>
const
&);
};
无论是继续使用 T
还是省略掉模板参数声明,都不可以(要么是里面的
T
隐藏了外面的
T
,
要么是在命名空间作用域内声明了一个非模板函数)。
2.5 模板类的特例化
可以对类模板的某一个模板参数进行特化。和函数模板的重载(参见 1.5
节)类似,类模板
的特化允许我们对某一特定类型做优化,或者去修正类模板针对某一特定类型实例化之后的
行为。不过如果对类模板进行了特化,那么也需要去特化所有的成员函数。虽然允许只特例
化模板类的一个成员函数,不过一旦你这样做,你就无法再去特化那些未被特化的部分了。
为了特化一个类模板,在类模板声明的前面需要有一个 template<>
,并且需要指明所希望特
化的类型。这些用于特化类模板的类型被用作模板参数,并且需要紧跟在类名的后面:
template
<>
class
Stack
<
std
::
string
> {
…
};
对于被特化的模板,所有成员函数的定义都应该被定义成“常规”成员函数,也就是说所有
出现
T
的地方,都应该被替换成用于特化类模板的类型:
void Stack<std::string>::push (std::string const& elem)
{
elems.push_back(elem); // append copy of passed elem
}
template<>
class Stack<std::string> {
private:
std::deque<std::string> elems; // elements
public:
void push(std::string const&); // push element
void pop(); // pop element
std::string const& top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
void Stack<std::string>::push(std::string const& elem)
{
elems.push_back(elem); // append copy of passed elem
}
void Stack<std::string>::pop()
{
assert(!elems.empty());
elems.pop_back(); // remove last element
}
std::string const& Stack<std::string>::top() const
{
assert(!elems.empty());
return elems.back(); // return copy of last element
}
在这个例子中,特例化之后的类在向 push()
传递参数的时候使用了引用语义,对当前
std::string
类型这是有意义的,这可以提高性能(如果使用
forwarding reference
【
Effective
Modern C++
解释了和万能引用
(Universal Reference
的异同
)
】传递参数的话会更好一些,
6.1
节会介绍这一内容)。
另一个不同是使用了一个 deque
而不再是
vector
来存储
stack
里面的元素。虽然这样做可能
不会有什么好处,不过这能够说明,模板类特例化之后的实现可能和模板类的原始实现有很
大不同。
2.6 部分特例化
类模板可以只被部分的特例化。这样就可以为某些特殊情况提供特殊的实现,不过使用者还
是要定义一部分模板参数。比如,可以特殊化一个
Stack<>
来专门处理指针:
// partial specialization of class Stack<> for pointers:
template<typename T>
class Stack<T*> {
private:
std::vector<T*> elems; // elements
public:
void push(T*); // push element
T* pop(); // pop element
T* top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
template<typename T>
void Stack<T*>::push(T* elem)
{
elems.push_back(elem); // append copy of passed elem
}
template<typename T>
T* Stack<T*>::pop()
{
assert(!elems.empty());
T* p = elems.back();
elems.pop_back(); // remove last element
return p; // and return it (unlike in the general case)
}
template<typename T>
T* Stack<T*>::top() const
{
assert(!elems.empty());
return elems.back(); // return copy of last element
}
2.7 默认类模板参数
和函数模板一样,也可以给类模板的模板参数指定默认值。比如对 Stack<>
,你可以将其用
来容纳元素的容器声明为第二个模板参数,并指定其默认值是
std::vector<>:
template<typename T, typename Cont = std::vector<T>>
class Stack {
private:
Cont elems; // elements
public:
void push(T const& elem); // push element
void pop(); // pop element
T const& top() const; // return top element
bool empty() const { // return whether the stack is
return elems.empty();
}
};
template<typename T, typename Cont>
void Stack<T, Cont>::push(T const& elem)
{
elems.push_back(elem); // append copy of passed elem
}
template<typename T, typename Cont>
void Stack<T, Cont>::pop()
{
assert(!elems.empty());
elems.pop_back(); // remove last element
}
template<typename T, typename Cont>
T const& Stack<T, Cont>::top() const
{
assert(!elems.empty());
return elems.back(); // return copy of last element
}
由于现在有两个模板参数,因此每个成员函数的定义也应该包含两个模板参数:
template
<
typename
T
,
typename
Cont
>
2.8 类型别名(Type Aliases)
通过给类模板定义一个新的名字,可以使类模板的使用变得更方便。
Typedefs 和 Alias 声明
为了简化给类模板定义新名字的过程,有两种方法可用:
1.
使用关键字
typedef:
typedef
Stack
<
int
>
IntStack
;
// typedef
void
foo
(
IntStack
const
&
s
);
// s is stack of ints
IntStack istack
[
10
];
// istack is array of 10 stacks of ints
我们称这种声明方式为 typedef
, 被定义的名字叫做
typedef-name
2.
使用关键字
using
(从
C++11
开始)
using
IntStack
=
Stack
<
int
>;
// alias declaration
void
foo
(
IntStack
const
&
s
);
// s is stack of ints
IntStack istack
[
10
];
// istack is array of 10 stacks of ints
按照[DosReisMarcusAliasTemplates]
的说法,这一过程叫做
alias declaration
。在这两种情况下我们都只是为一个已经存在的类型定义了一个别名,并没有定义新的类型。因此在:
typedef
Stack
<
int
>
IntStack
;
或者:
using
IntStack
=
Stack
<
int
>;
之后,
IntStack
和
Stack<int>
将是两个等效的符号。
以上两种给一个已经存在的类型定义新名字的方式,被称为 type alias declaration
。新的名字
被称为
type alias
。
Alias Templates(别名模板)
不同于 typedef
,
alias declaration
也可以被模板化,这样就可以给一组类型取一个方便的名
字。这一特性从
C++11
开始生效,被称作
alias templates
。
下面的 DequeStack
别名模板是被元素类型
T
参数化的,代表将其元素存储在
std::deque
中
的一组
Stack
:
template
<
typename
T
>
using
DequeStack
=
Stack
<
T
,
std
::
deque
<
T
>>;
因此,类模板和 alias templates
都是可以被参数化的类型。同样地,这里
alias template
只是 一个已经存在的类型的新名字,原来的名字依然可用。DequeStack<int>
和
Stack<int,
std::deque<int>>
代表的是同一种类型。
同样的,通常模板(包含
Alias Templates
)只可以被声明和定义在
global/namespace
作用域,
或者在一个类的声明中。
Alias Templates for Member Types(class 成员的别名模板)
下面这样的定义:
template
<
typename
T
>
using
MyTypeIterator
=
typename
MyType
<
T
>::
iterator
;
允许我们使用:
MyTypeIterator
<
int
>
pos
;
取代:
typename
MyType
<
T
>::
iterator pos
;
2.9 类模板的类型推导
如果构造函数能够推断出所有模板参数的类型(对 那些没有默认值的模板参数),就不再需要显式的指明模板参数的类型。
比如在之前所有的例子中,不指定模板类型就可以调用
copy constructor:
Stack< int> intStack1; // stack of strings
Stack< int> intStack2 = intStack1; // OK in all versions
Stack intStack3 = intStack1; // OK since C++17
通过提供一个接受初始化参数的构造函数,就可以推断出 Stack
的元素类型。比如可以定义
下面这样一个
Stack
,它可以被一个元素初始化:
template<typename T>
class Stack {
private:
std::vector<T> elems; // elements
public:
Stack () = default;
Stack (T const& elem) // initialize stack with one element
: elems({elem}) {
}
…
};
通过用 0
初始化这个
stack
时,模板参数
T
被推断为
int
,这样就会实例化出一个
Stack<int>
。
但是请注意下面这些细节:
1.由于定义了接受 int
作为参数的构造函数,要记得向编译器要求生成默认构造函数及其
全部默认行为,这是因为默认构造函数只有在没有定义其它构造函数的情况下才会默认生成,方法如下: Stack
() =
default
;
2 在初始化 Stack
的
vector
成员
elems
时,参数
elem
被用
{}
括了起来,这相当于用只有一
个元素
elem
的初始化列表初始化了
elems:
:
elems
({
elem
})
这是因为 vector
没有可以直接接受一个参数的构造函数。
和函数模板不同,类模板可能无法部分的推断模板类型参数(比如在显式的指定了一部分类
模板参数的情况下)。具体细节请参见
15.12
节
类模板对字符串常量参数的类型推断(Class Template Arguments Deduction with String Literals )
原则上,可以通过字符串常量来初始化
Stack
:
Stack stringStack
=
"bottom"
;
// Stack<char const[7]> deduced since C++17
不过这样会带来一堆问题:当参数是按照 T
的引用传递的时候(上面例子中接受一个参数的
构造函数,是按照引用传递的),参数类型不会被
decay
,也就是说一个裸的数组类型不会
被转换成裸指针。这样我们就等于初始化了一个这样的
Stack:
Stack
<
char const
[
7
]>
类模板中的 T
都会被实例化成
char const[7]
。这样就不能继续向
Stack
追加一个不同维度的
字符串常量了,因为它的类型不是
char const[7]
。详细的讨论请参见
7.4
节。
不过如果参数是按值传递的,参数类型就会被 decay
,也就是说会将裸数组退化成裸指针。
这样构造函数的参数类型
T
会被推断为
char const *
,实例化后的类模板类型会被推断为
Stack<char const *>
。
基于以上原因,可能有必要将构造函数声明成按值传递参数的形式:
template<typename T>
class Stack {
private:
std::vector<T> elems; // elements
public:
Stack (T elem) // initialize stack with one element by value
: elems({elem}) { // to decay on class tmpl arg deduction
}
…
};
这样下面的初始化方式就可以正常工作:
Stack stringStack
=
"bottom"
;
// Stack<char const*> deduced since C++17
在这个例子中,最好将临时变量 elem move
到
stack
中,这样可以免除不必要的拷贝
:
template<typename T>
class Stack {
private:
std::vector<T> elems; // elements
public:
Stack (T elem) // initialize stack with one element by value
: elems({std::move(elem)}) {
}
…
};
第 3 章 非类型模板参数
3.1 类模板的非类型参数
作为和之前章节中 Stack
实现方式的对比,可以定义一个使用固定尺寸的
array
作为容器的
Stack
。这种方式的优点是可以避免由开发者或者标准库容器负责的内存管理开销。不过对
不同应用,这一固定尺寸的具体大小也很难确定。如果指定的值过小,那么
Stack
就会很容
易满。如果指定的值过大,则可能造成内存浪费。因此最好是让
Stack
的用户根据自身情况
指定
Stack
的大小。
为此,可以将 Stack
的大小定义成模板的参数:
// testtemplate.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <vector>
#include <deque>
#include <string>
#include <cassert>
#include <array>
#include <cassert>
template<typename T, std::size_t Maxsize>
class Stack {
private:
std::array<T, Maxsize> elems; // elements
std::size_t numElems; // current number of elements
public:
Stack(); // constructor
void push(T const& elem); // push element
void pop(); // pop element
T const& top() const; // return top element
bool empty() const { //return whether the stack is empty
return numElems == 0;
}
std::size_t size() const { //return current number of elements
return numElems;
}
};
template<typename T, std::size_t Maxsize>
Stack<T, Maxsize>::Stack()
: numElems(0) //start with no elements
{// nothing else to do
}
template<typename T, std::size_t Maxsize>
void Stack<T, Maxsize>::push(T const& elem)
{
assert(numElems < Maxsize);
elems[numElems] = elem; // append element
++numElems; // increment number of elements
}
template<typename T, std::size_t Maxsize>
void Stack<T, Maxsize>::pop()
{
assert(!elems.empty());
--numElems; // decrement number of elements
}
template<typename T, std::size_t Maxsize>
T const& Stack<T, Maxsize>::top() const
{
assert(!elems.empty());
return elems[numElems - 1]; // return last element
}
int main()
{
Stack<int, 20> int20Stack; // stack of up to 20 ints
Stack<int, 40> int40Stack; // stack of up to 40 ints
Stack<std::string, 40> stringStack; // stack of up to 40 strings
// manipulate stack of up to 20 ints
int20Stack.push(7);
int20Stack.pop();
// manipulate stack of up to 40 strings
stringStack.push("hello");
stringStack.pop();
return 0;
}
上面每一次模板的使用都会实例化出一个新的类型。因此 int20Stack
和
int40Stack
是两种不
同的类型,而且由于它们之间没有定义隐式或者显式的类型转换规则。也就不能使用其中一
个取代另一个,或者将其中一个赋值给另一个。
对非类型模板参数,也可以指定默认值:
template
<
typename
T
=
int
,
std
::
size_t
Maxsize
=
100
>
class
Stack
{
…
};
但是从程序设计的角度来看,这可能不是一个好的设计方案。默认值应该是直观上正确的。
不过对于一个普通的
Stack
,无论是默认的
int
类型还是
Stack
的最大尺寸
100
,看上去都不
够直观。因此最好是让程序员同时显式地指定两个模板参数,这样在声明的时候这两个模板
参数通常都会被文档化。
3.2 函数模板的非类型参数
同样也可以给函数模板定义非类型模板参数。比如下面的这个函数模板,定义了一组可以返
回传入参数和某个值之和的函数:
template<int Val, typename T>
T addValue (T x)
{
return x + Val;
}
当该类函数或操作是被用作其它函数的参数时,可能会很有用。比如当使用 C++
标准库给一
个集合中的所有元素增加某个值的时候,可以将这个函数模板的一个实例化版本用作第
4 个参数
std
::
transform
(
source
.
begin
(),
source
.
end
(),
//start and end of source
dest
.
begin
(),
//start of destination
addValue
<
5
,
int
>);
// operation
第 4
个参数是从
addValue<>()
实例化出一个可以给传入的
int
型参数加
5
的函数实例。这一
实例会被用来处理集合
source
中的所有元素,并将结果保存到目标集合
dest
中。
注意在这里必须将 addValue<>()
的模板参数
T
指定为
int
类型。因为类型推断只会对立即发
生的调用起作用,而
std::transform()
又需要一个完整的类型来推断其第四个参数的类型。目
前还不支持先部分地替换或者推断模板参数的类型,然后再基于具体情况去推断其余的模板
参数。
3.3 非类型模板参数的限制
使用非类型模板参数是有限制的。通常它们只能是整形常量(包含枚举),指向
objects/functions/members
的指针,
objects
或者
functions
的左值引用,或者是
std::nullptr_t
(类型是
nullptr
)。
浮点型数值
或者
class 类型
的对象都不能作为非类型模板参数使用:
template<double VAT> // ERROR: floating-point values are not
double process (double v) // allowed as template parameters
{
return v * VAT;
}
template<std::string name> // ERROR: class-type objects are not
class MyClass { // allowed as template parameters …
}
第 4 章 变参模板
4.1 变参模板
可以将模板参数定义成能够接受任意多个模板参数的情况。这一类模板被称为变参模板
(
variadic template
)。
4.1.1 变参模板实列
// testtemplate.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
void print()
{}
template<typename T, typename ... Types>
void print(T firstArg, Types... args)
{
std::cout << firstArg << std::endl; //print first argument
print(args...); // call print() for remaining arguments
}
int main()
{
std::string s("world");
print(7.5, "hello", s);
return 0;
}
如果传入的参数是一个或者多个,就会调用这个函数模板,这里通过将第一个参数单独声明, 就可以先打印第一个参数,然后再递归的调用 print()
来打印剩余的参数。这些被称为
args
的剩余参数,是一个函数参数包(
function parameter pack
):
void
print
(
T firstArg
,
Types
…
args
)
这里使用了通过模板参数包(
template parameter pack
)定义的类型“
Types
”:
template
<
typename
T
,
typename
…
Types
>
为了结束递归,重载了不接受参数的非模板函数
print()
,它会在参数包为空的时候被调用。
在调用中:
因为这个调用首先会被扩展成:
print
<
double
,
char const
*,
std
::
string
> (
7.5
,
"hello"
,
s
);
其中:
- firstArg 的值是 7.5, 其类型 T 是 double。
- args 是一个可变模板参数,它包含类型是 char const*的“hello”和类型是 std::string 的 “world”
在打印了 firstArg
对应的
7.5
之后,继续调用
print()
打印剩余的参数,这时
print()
被扩展为:
print
<
char const
*,
std
::
string
> (
"hello"
,
s
)
其中:
- firstArg 的值是“hello”,其类型 T 是 char const *。
- args 是一个可变模板参数,它包含的参数类型是 std::string。
在打印了
firstArg
对应的“
hello
”之后,继续调用
print()
打印剩余的参数,这时
print()
被扩
展为:
print
<
std
::
string
> (
s
);
其中:
- firstArg 的值是“world”,其类型 T 是 std::string。
- args 是一个空的可变模板参数,它没有任何值。
这样在打印了
firstArg
对应的“
world
”之后,就会调用被重载的不接受参数的非模板函数
print()
,从而结束了递归。
4.1.2 变参和非变参模板的重载
上面的例子也可以这样实现:
// testtemplate.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
template<typename T>
void print(T arg)
{
std::cout << arg << std::endl;
}
template<typename T, typename ... Types>
void print(T firstArg, Types... args)
{
print(firstArg);
print(args...); // call print() for remaining arguments
}
int main()
{
std::string s("world");
print(7.5, "hello", s);
return 0;
}
也就是说,当两个函数模板的区别只在于尾部的参数包的时候,会优先选择没有尾部参数包
的那一个函数模板。相关的、更详细的重载解析规则请参见
C3.1
节。
4.1.3 sizeof... 运算符
C++11 为变参模板引入了一种新的
sizeof
运算符:
sizeof...
。它会被扩展成参数包中所包含的
参数数目
这样可能会让你觉得,可以不使用为了结束递归而重载的不接受参数的非模板函数 print()
,
只要在没有参数的时候不去调用任何函数就可以了:
template<typename T, typename… Types>
void print (T firstArg, Types… args)
{
std::cout << firstArg << ’\n’;
if (sizeof…(args) > 0) { //error if sizeof…(args)==0
print(args…); // and no print() for no arguments declared
}
}
但是这一方式是错误的,因为通常函数模板中 if
语句的两个分支都会被实例化。是否使用被
实例化出来的代码是在运行期间(
run-time
)决定的,而是否实例化代码是在编译期间
(
compile-time
)决定的。因此如果在只有一个参数的时候调用
print()
函数模板,虽然
args...
为空,if 语句中的
print(args...)
也依然会被实例化,但此时没有定义不接受参数的
print()
函数,
因此会报错。
而 C++17 又在此基础上引入了同样可以在编译期基于某些条件禁用或者启用相应模板的编译期 if
语 句。通过使用 if constexpr(...)
语法,编译器会使用编译期表达式来决定是使用
if
语句的
then 对应的部分还是 else
对应的部分。
template<typename T, typename ... Types>
void print(T const& firstArg, Types const&... args)
{
std::cout << firstArg << "\n";
if constexpr (sizeof...(args) > 0) {
print(args...);
}
}
这里如果只给 print()
传递一个参数,那么
args...
就是一个空的参数包,此时
sizeof...(args)
等于 0。这样
if
语句里面的语句就会被丢弃掉,也就是说这部分代码不会被实例化。因此也就不
再需要一个单独的函数来终结递归。
4.2 折叠表达式
从 C++17
开始,提供了一种可以用来计算参数包(可以有初始值)中所有参数运算结果的二
元运算符。
// testtemplate.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
template<typename ...args>
auto foldSum(args... arg) {
return (... + arg);
}
int main()
{
std::cout << foldSum(1, 2, 3);
return 0;
}
如果参数包是空的,这个表达式将是不合规范的(不过此时对于运算符&&
,结果会是
true
,
对运算符
||
,结果会是
false
,对于逗号运算符,结果会是
void()
)。
通过这样一个使用了初始化器的折叠表达式,似乎可以简化打印变参模板参数的过程,像上
面那样:
template<typename ...Types>
auto print(Types... args) {
(std::cout << ... << args) << "\n";
}
不过这样在参数包各元素之间并不会打印空格。为了打印空格,还需要下面这样一个类模板,它可以在所有要打印的参数后面追加一个空格:
// testtemplate.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
template<typename T>
class AddSpace {
public:
AddSpace(T const& r) : ref(r) {
}
friend std::ostream& operator<< (std::ostream& os, AddSpace<T> s) {
return os << s.ref << " "; // output passed argument and a space
}
private:
T const& ref;
};
template<typename... Args>
void print(Args... args) {
(std::cout << ... << AddSpace<Args>(args)) << "\n";
}
int main()
{
print(1,2,3,"4");
return 0;
}
4.3 变参模板的使用
变参模板在泛型库的开发中有重要的作用,比如 C++
标准库。
一个重要的作用是转发任意类型和数量的参数。比如在如下情况下会使用这一特性:
一个重要的作用是转发任意类型和数量的参数。比如在如下情况下会使用这一特性:
- 向一个由智能指针管理的,在堆中创建的对象的构造函数传递参数:// create shared pointer to complex<float> initialized by 4.2 and 7.7: auto sp = std::make_shared<std::complex<float>>(4.2, 7.7);
- 向一个由库启动的 thread 传递参数: std::thread t (foo, 42, "hello"); //call foo(42,"hello") in a separate thread
- 向一个被 push 进 vector 中的对象的构造函数传递参数: std::vector<Customer> v; … v.emplace("Tim", "Jovi", 1962); //insert a Customer initialized by three arguments
通常是使用移动语义对参数进行完美转发(perfectly forwarded
)(参见
6.1
节),它们像下
面这样进行声明:
4.4.1 变参表达式
第 5 章 基础技巧
5.1 typename 关键字
关键字typename
在
C++
标准化过程中被引入进来,用来澄清模板内部的一个标识符代表的
是某种类型,而不是数据成员。考虑下面这个例子:
template<typename T>
class MyClass {
public:
void foo() {
typename T::SubType* ptr;
}
};
其中第二个 typename
被用来澄清
SubType
是定义在
class T
中的一个类型。因此在这里
ptr
是一个指向
T::SubType
类型的指针。
如果没有 typename
的话,
SubType
会被假设成一个非类型成员(比如
static
成员或者一个枚举常量,亦或者是内部嵌套类或者 using
声明的
public
别名)。这样的话,表达式
T::SubType* ptr 会被理解成 class T
的
static
成员
SubType
与
ptr
的乘法运算,这不是一个错误,因为对 MyClass<>的某些实例化版本而言,这可能是有效的代码。
通常而言,当一个依赖于模板参数的名称代表的是某种类型的时候,就必须使用 typename
。
13.3.2
节会对这一内容做进一步的讨论。
使用 typename
的一种场景是用来声明泛型代码中标准容器的迭代器:
// 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";
}
int main()
{
std::string test = "hello";
printcoll(test);
return 0;
}
5.2零初始化
对于基础类型,比如int
,
double
以及指针类型,由于它们没有默认构造函数,因此它们不
会被默认初始化成一个有意义的值。比如任何未被初始化的局部变量的值都是未定义的:
void foo()
{
int x; // x has undefined value
int* ptr; // ptr points to anywhere (instead of nowhere)
}
因此在定义模板时,如果想让一个模板类型的变量被初始化成一个默认值,那么只是简单的
定义是不够的,因为对内置类型,它们不会被初始化:
template<typename T>
void foo()
{
T x; // x has undefined value if T is built-in type
}
正确做法
void foo()
{
int x{}; // x has undefined value
int* ptr{}; // ptr points to anywhere (instead of nowhere)
std::cout << x << " " << ptr;
}
出于这个原因,对于内置类型,最好显式的调用其默认构造函数来将它们初始化成 0(对于
bool
类型,初始化为
false
,对于指针类型,初始化成
nullptr
)。通过下面你的写法就可以
保证即使是内置类型也可以得到适当的初始化:
template<typename T>
void foo()
{
T x{}; // x is zero (or false) if T is a built-in type
}
这种初始化的方法被称为“值初始化(value initialization
)”,它要么调用一个对象已有的
构造函数,要么就用零来初始化这个对象。即使它有显式的构造函数也是这样。
对于用花括号初始 化的情况,如果没有可用的默认构造函数,它还可以使用列表初始化构造函数(initializer-list constructor)。
从
C++11
开始也可以通过如下方式对非静态成员进行默认初始化:
template<typename T>
class MyClass {
private:
T x{}; // zero-initialize x unless otherwise specified …
};
模版参数默认值
template<typename T>
void foo(T p = T{}) { //OK (must use T() before C++11) …
}
5.3 使用 this->
对于类模板,如果它的基类也是依赖于模板参数的,那么对它而言即使
x
是继承而来的,使
用
this->x
和
x
也不一定是等效的。比如:
template<typename T>
class Base {
public:
void bar();
};
template<typename T>
class Derived : Base<T> {
public:
void foo() {
bar(); // calls external bar() or error
}
};
Derived 中的
bar()
永远不会被解析成
Base
中的
bar()
。因此这样做要么会遇到错误,要么就
是调用了其它地方的
bar()
(比如可能是定义在其它地方的
global
的
bar()
)。
13.4.2 节对这一问题有更详细的讨论。目前作为经验法则,建议当使用定义于基类中的、依
赖于模板参数的成员时,用
this->
或者
Base<T>::
来修饰它。