EMCPP条款1:理解模板型别推导

YiQi 管理员

整理自“Effective Modern C++”.

  • 中文翻译不准确的或者不易于理解的地方使用英文原文代替

型别推导的目的是使人们不必再去写下那些不言自明或是完全冗余的型别。它还让C++软件获得更高的适应性,因为在源代码的一个地方对一个型别实施的改动,可以自动通过型别推导传播到其他地方。

用一小段伪代码来说明,函数模板大致形如:

1
2
3
4
template<typename T>
void f(ParamType param);

f(expr) // 从expr来推导T和ParamType的型别

T的型别推导结果要分三种情况讨论:

  • ParamType具有指针或引用型别,但不是个万能引用(万能引用会在条款24中介绍,现在你只需知道有这么个东西,并且它们与左值引用和右值引用都有所区别即可)。
  • ParamType是一个万能引用。
  • ParamType既非指针也非引用。

情况1: ParamType是个指针或引用,但不是个万能引用

  1. expr具有引用型别,先将引用部分忽略。
  2. 尔后,对expr的型别和ParamType的型别执行模式匹配,来决定T的型别。

例如,我们的模式如下:

1
2
template<typename T>
void f(T& param); // param 是个引用

又声明了下列变量:

1
2
3
int x = 27;         // x 的型别是 int
const int cx= x; // cx 的型别是 const int
const int& rx = x; // rx 是 x 的型别为 const int 的引用

在各次调用中,对paramT的型别推导结果如下:

1
2
3
f(x);   // T 的型别是 int. param 的型别是 int&
f(cx); // T 的型别是 const int, param 的型别是 const int&
f(rx); // T 的和别是 const int, param 的和别是 const int&

尽量上述调用语句示例演示的都是左值引用形参,但是右值引用形参的型别推导运作方式是完全相同的。当然,传给右值引用形参的,只能是右值引用实参,但这个限制和型别推导无关。

如果我们将形参型别从T&改为const T&,结果会有一点变化,但这些变化并没有什么出人意料之处,cxrx的常量性仍然得到了满足:

1
2
3
4
5
6
7
8
9
10
template<typename T>
void f(const T& param); // param 是个引用

int x = 27; // x 的型别是 int
const int cx= x; // cx 的型别是 const int
const int& rx = x; // rx 是 x 的型别为 const int 的引用

f(x); // T 的型别是 int. param 的型别是 const int&
f(cx); // T 的型别是 int, param 的型别是 const int&
f(rx); // T 的和别是 int, param 的和别是 const int&

如果param是个指针(或指涉到const对象的指针)而非引用,运作方式本质上并无不同:

1
2
3
4
5
6
7
8
template<typename T>
void f(T* param); // param is now a pointer

int x = 27; // as before
const int *px = &x; // px is a ptr to x as a const int

f(&x); // T is int, param's type is int*
f(px); // T is const int, param's type is const int*

情况2: ParamType是个万能引用

  • 如果expr是个左值,TParamType都会被推导为左值引用。这个结果具有双重的奇特之处:首先,这是在模板型别推导中,T被推导为引用型别的唯一情形。其次,尽管在声明时使用的是右值引用语法,它的型别推导结果却是左值引用。
  • 如果expr是个右值,则应用“常规"(即情况1中的)规则。
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void f(T&& param); // param is now a universal reference

int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before

f(x); // x is lvalue, so T is int&, param's type is also int&
f(cx); // cx is lvalue, so T is const int&, param's type is also const int&
f(rx); // rx is lvalue, so T is const int&, param's type is also const int&
f(27); // 27 is rvalue, so T is int, param's type is therefore int&&

当遇到万能引用时,型别推导规则会区分实参是左值还是右值。而非万能引用是从来不会作这样的区分的。

情况3: ParamType既非指针也非引用

ParamType既非指针也非引用时 ,我们面对的就是所谓按值传递了:

1
2
template<typename T>
void f(T param); // param 现在是按值传递

param会是个全新对象这一事实促成了如何从expr推导出T的型别的规则:

  • 一如之前,若expr具有引用型别,则忽略其引用部分。
  • 忽略expr的引用性之后,若expr是个const对象,也忽略之。若其是个volatile对象,同忽略之(volatile对象不常用,它们 一般仅用千实现设备驱动程序。欲知详情,参见条款 40)。
    1
    2
    3
    4
    5
    6
    7
    int x = 27;         // as before
    const int cx = x; // as before
    const int& rx = x; // as before

    f(x); // T's and param's types are both int
    f(cx); // T's and param's types are again both int
    f(rx); // T's and param's types are still both int
    仅仅由于expr不可修改,并不能断定其副本也不可修改。需要重点说明的是,const(和volatile)仅会在按值形参处被忽略。正如此前所见,若形参是const的引用或指针(references-to- or pointers-to const),expr的常量性会在型别推导过程中加以保留。但是,考虑这种情况:expr是个指涉到const对象的const指针,且expr按值传给param:
    1
    2
    3
    4
    5
    6
    7
    template<typename T>
    void f(T param); // param is still passed by value

    const char* const ptr = "Fun with pointers";
    // ptr is const pointer to const object

    f(ptr); // pass arg of type const char * const
    左边const表示ptr指向的内容不能修改,右侧const表示ptr本身的值不能修改。按值传递时,其自身的const属性被忽略,所以param型别会被推导为const char*。The constness of what ptr points to is preserved during type deduction, but the constness of ptr itself is ignored when copying it to create the new pointer, param.

数组实参

首先,在很多语境下,数组会退化成指涉到其首元素的指针:

1
2
const char name[] = "J. P. Briggs"; // name 的型别是 c on st char [1 3]
const char * ptrToName = name; // 数组退化成指针

但当一个数组传递给持有按值形参的模板时,

1
2
3
template<typename T>
void f(T param); // 持有按值形参的模板
f(name); // T和param的型别会被推导成什么呢?

由于数组形参声明会按照它们好像是指针形参那样加以处理,按值传递给函数模板的数组型别将被推导成指针型别。也就是说,在模板 f 的调用中,其型别形参 T 会被推导成 const char*:

1
f(name); // name 是个数组,但 T 的型别却披推导成 const char *

尽管函数无法声明真正的数组型别的形参,它们却能够将形参声明成数组的引用!所以,如果我们修改模板 f, 指定按引用方式传递其实参,

1
2
3
template<typename T>
void f(T& param); // 按引用方式传递形参的换板
f(name); // 向 f 传递-个数组

在这种情况下,T 的型别会被推导成实际的数组型别!这个型别中会包含数组尺寸,在本例中,T 的型别推导结果是 const char [13],而 f 的形参(该数组的一个引用)型别则被推导为 const char (&)[13]

有意思的是,可以利用声明数组引用这一能力创造出一个模板,用来推导出数组含有的元素个数:

1
2
3
4
5
6
7
// return size of an array as a compile-time constant. (The
// array parameter has no name, because we care only about
// the number of elements it contains.)
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept {
return N;
}

函数实参

函数型别也同样会退化成函数指针,并且我们针对数组型别推导的一切讨论都适用千函数及其向函数指针的退化。所以结果如
下:

1
2
3
4
5
6
7
8
9
10
void someFunc(int, double); 
// someFunc is a function. type is void(int, double)
template<typename T>
void f1(T param); // in f1, param passed by value
template<typename T>
void f2(T& param); // in f2, param passed by ref
f1(someFunc);
// param deduced as ptr-to-func; type is void (*)(int, double)
f2(someFunc);
// param deduced as ref-to-func; type is void (&)(int, double)

要点速记

  • 在模板型别推导过程中,具有引用型别的实参会被当成非引用型别来处理。换言之,其引用性会被忽略。
  • 对万能引用形参进行推导时,左值实参会进行特殊处理。
  • 对按值传递的形参进行推导时,若实参型别中带有 constvolatile 饰词,则它们还是会被当作不带 constvolatile 饰词的型别来处理。
  • 在模板型别推导过程中,数组或函数型别的实参会退化成对应的指针,除非它们被用来初始化引用。