首页 > 嗟来之食 > 《Effective Modern C++》读书笔记 Item 1 理解模板类型推导 – Presia –
2016
08-12

《Effective Modern C++》读书笔记 Item 1 理解模板类型推导 – Presia –

最近发现了《Effective Modern C++》这本书,作者正是大名鼎鼎的Scott Meyers——《Effective C++》、《Effective STL》的作者。
而就在C++11逐渐普及,甚至是C++14的新特性也进入大家的视野的时候,《Effective Modern C++》一书应运而生。此书与其前辈一样,通过数十个条款来展开,只不过这次是集中于C++11和C++14的新特性。auto、decltype、move、lambda表达式……这些强而有力的新特性背后到底隐藏着哪些细节和要点?……
阅读这本书的时候,感受到的豁然开朗的愉悦与初学C++时看Scott前几本著作时别无二致。遂尝试摘录一二,结合所想,做些记录,同时也试着检查一些自己的知识点有哪些欠缺,希望大家能多多指正。

注意:

蛤蛤蛤蛤蛤

↑↑↑↑ 这样的方框里的片段完全不来自于原书,而是我自己的理解。

Item 1 Understand template type deduction – 理解模板类型推导
模板类型推导是C++长期以来的特性。比如:

template<typename T>
void f(ParamType param);

f(expr); // 调用 f

其中 ParamType 可以是和 T 有关的类型,只不过包含一些修饰,比如 const 或引用修饰符(reference qualifier)。如:

template<typename T>
void f(const T& param); // ParamType 为 const T&

对于这样的调用:

int x = 0;
f(x);

一个特化(Specialize)的函数就经由类型推导生成了,T 被推导(deduce)为 int,ParamType 则被推导为 const int& 。

上面这种过程是类型推导,而

template<> void f<int>(int);

就不算类型推导了&mdash;&mdash;因为并没有进行“类型推导”,而是直接指定了&mdash;&mdash;cppreference上将这叫做instantiate,实例化。编译器将特化有特定模板参数的函数模板。

在这种形式中,T 的推导不仅依赖于 expr 的类型,还和 ParamType 的形式(form)有关。对此书中给出三种情形:

情形1:ParamType 是指针(pointer)或引用(reference)类型,但不是universal引用(universal reference)
(此时书中并未详述什么叫universal引用,不过对此情形影响不大,因为universal引用首先就不是左值引用,即不是形如 int&、T&)
在第一种情形下,类型推导有如下规则:

若 expr 类型是引用,忽略引用部分;
这之后,把 expr 的类型对 ParamType 进行模式匹配来决定 T。

上两条中,expr 指的就是函数的实参(argument),而 ParamType 是形参(parameter)的类型。书中例子为:

template<typename T>
void f(T& param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x); // T -> int, ParamType -> int&
f(cx); // T -> const int, ParamType -> const int&
f(rx); // T -> const int, ParamType -> const int&

expr,即上例的 x、cx、rx,去掉引用部分后为 int,const int,而 param 将要对这几种类型的变量建立引用,ParamType 就推导出了上述的结果。
其中很重要的一点:
当传递一个 const 对象到一个引用参数(parameter)时,调用者希望这个对象能保持 const 特性,即不变性。
模板类型推导遵从这一要点。故传递 const 对象到模板参数 T& 是安全的,不会丢失 const 属性。
并且以上规则对于右值引用也是成立的。
而将上例中的 ParamType 改为 const T& 时,上例三次调用全部将 ParamType 推导为 const int&,T 则每次都为 int。因为 ParamType 的形式中带有了 const,匹配后 T 就不需要带有 const 了。

而对于 ParamType 是指针的情形,推导过程也是同样的。只是去除了“忽略引用部分”这一步,只是对指针类型进行模式匹配。

情形2:ParamType 是universal引用
模板函数的参数是universal引用的时候,比如“像是”右值(rvalue)引用,即 T&& 这样的类型,其中 T 的模板类型参数。

我想,所谓universal引用,可以先参考“引用叠加效果”表:
& & -> && && -> &&& & -> &&& && -> &&
我想这可能念做:&引用的&引用是&引用,&引用的&&引用是&引用,&&引用的&引用是&引用&hellip;&hellip;
我的理解是,其中任一种引用的&&引用都是原型,所以叫做universal引用吧。由于是叠加在未确定的模板类型 T 上的,所以写法虽然一样,但并不是右值引用,因为右值引用是作用于明确类型上的。
参考资料 http://stackoverflow.com/questions/20364297/why-universal-references-have-the-same-syntax-as-rvalue-references

对于universal引用,类型推导规则为:

如果 expr 是左值(lvalue),则 T 和 ParamType 都推导为左值引用;
如果 expr 是右值(rvalue),则“普通”规则&mdash;&mdash;第一种情形的规则被应用。

规则1可以对照引用叠加表,expr 的类型就是 -> 号右边的,如果它是左值即&,能通过universal引用变成这样状态的也只有左值&。即使 ParamType 被声明为和右值引用类似的形式,ParamType 本身也被推导为左值引用。

我认为这一推导正是由于待定的 ParamType 并不能表示一个右值引用类型,而只能作为一种“带有未知量 T”的类型运算表达式。比如 T 若是 int&&,则 T& 就是 int&。

书中对于情形2的例子为:

template<typename T>
void f(T&& param); // param is now a universal reference
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // x -> lvalue, T -> int&, ParamType -> int&
f(cx); // cx -> lvalue, T -> const int&, ParamType -> const int&
f(rx); // rx -> lvalue, T -> const int&, ParamType -> const int&
f(27); // 27 -> rvalue, T -> int, ParamType -> int&&

第4个调用即为退回情形1规则的情况。expr 是一个右值,则进行模式匹配后被绑定到 int&&,其中 T 为 int。

情形3:ParamType 既非指针,也非引用
就像这样,按值传递/按拷贝传递(pass-by-value):

template<typename T>
void f(T param);

那么 param 总是对实参(argument)进行拷贝。此情形有规则:

如果 expr 类型是引用,忽略引用部分;
之后,如果 expr 是 const,忽略 const。如果还是 volatile,也忽略这个。

int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T and ParamTypes -> int
f(cx); // T and ParamTypes -> int
f(rx); // T and ParamTypes -> int

因为 param 总是 expr 的拷贝,所以无论怎样都不会影响 expr,所以 expr 的 const、volatile 这些特性,都和 param 无关了。

这也正符合上面所说的,调用者希望传入的对象原本具有的特性(如 const)不受影响,程序的实现要遵从这一希望。

原书在这里举了一个 const char * const 按拷贝传递(按值传递&mdash;&mdash;相对于按引用传递)的例子,不细表。

数组作为实参
C/C++都有这样一个特性,那就是数组的退化(decay):

const char str[] = "hello"; // const char[6]
const char *p = str; // 数组退化为指针

很明显 str 和 p 的类型是不同的。而且对于C中的语法,是可以将函数的参数声明为数组的形式的,但是以下两者却是相同的:

void func(char str[]);
void func(char *str);

这是因为数组形式的形参(parameter),会被当作指针形式的形参处理。
因此对于按值传递的模板参数 T 来说,实参为数组 char[] 时,T 被推导为 char *。(可以认为数组的退化先发生。)

但模板参数为引用的时候,是能“真正”引用到传入的数组的(即不发生数组退化):

template<typename T>
void f(T& param);

f(str); // T -> const char[6], ParamType -> const char (&)[6]

一个例子,通过模板在编译期获取数组大小(代码中暂时无关的部分被去掉了):

template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N])
{
return N;
}

函数作为实参
除了数组之外,函数也会退回为指针。但同时,同样能通过模板提供引用类型参数来避免退化:

void someFunc(int, double); // someFunc -> void(int, double)
template<typename T>
void f1(T param);
template<typename T>
void f2(T& param);
f1(someFunc); // ParamType -> void (*)(int, double)
f2(someFunc); // ParamType -> void (&)(int, double)

数组和函数的退化都是针对其标识符。

此条款的注意点

类型推导中,引用类型的实参被视为非引用的。
对于universal引用形式的形参的类型推导,左值实参需要特殊处理。
按值传递的实参,其 const 和 volatile 都会被无视。
类型推导中,使用数组或函数的标识符时,如不将其用于初始化引用,就会导致其退化为指针。

最后编辑:
作者:
这个作者貌似有点懒,什么都没有留下。

留下一个回复