ROOT
ROOT
文章目录
  1. Item 1: Understand template type deduction
    1. Case 1: ParamType 是指针或者引用,但不是通用引用。
    2. Case 2: ParamType 是通用引用
    3. Case 3: ParamType 既不是引用也不是指针
    4. 数组作为参数
    5. 函数作为参数
    6. Things to Remember
  2. Item 2: Understand auto type deduction
    1. Things to Remember
  3. Item 3: Understand decltype
    1. Things to Remember
  4. Item 4: Know how to view deduced types
    1. IDE 编辑器
    2. 编译器诊断
    3. 运行时输出
    4. Things to Remember

《Effective Modern C++》第一章笔记:Deducing Types

这几天人工智能成绩出来了,还是开卷考试,竟然挂掉了,真是意不意外。。。请导员查了一下平时分:卷面 46/100,平时 53/100,按经验来说,只要卷面过 40,靠平时分还是能拉到及格的,以前平时分一般会给到 90 左右,这次真是吃了平时分的亏了。

回忆了一下,人工智能这门课不点名,但是每节课都会有随堂测试,靠这个来算平时分吧。这门课我印象中缺过两节课,因为前段时间在忙比赛,加上开卷,期间也和老师提前打过招呼(邮件没回复,估计没看到),随堂测试靠度娘。结果居然因为平时分太低也救不了了。

最近买了本动物书《Effective Modern C++》,好评,从头到尾指出了一些 C++11/14 的坑,同时介绍一些技巧如何避免这些坑。

这里做一下笔记,备忘。我用的是 clang 编译器。

这章主要是介绍 C++11/14 的推导规则,以及各种坑。

Item 1: Understand template type deduction

首先从模板类型推导开始,后面的 auto 关键字和模板类型推导规则差不多的。需要注意下面说的 parameter 就是形参,而 argument 是实参。

首先用如下伪代码介绍:

template<class T>
void f(ParamType param); // 模板函数

f(expr); // 调用 f,argument 是 expr,根据 expr 来确定 T 和 param 的类型。

其中 T 的类型不仅由 expr 来确定,还由 ParamType 来确定的。这里有 3 种情况:

  • ParamType 是一个指针或者引用,但不是通用引用。(这里可以先理解为T &&。)
  • ParamType 是通用引用。
  • ParamType 既不是引用也不是指针

Case 1: ParamType 是指针或者引用,但不是通用引用。

规则如下:

  1. 如果 expr 是引用,那就忽略其引用部分
  2. 根据 expr 的类型来匹配 ParamType 和 T

Case 2: ParamType 是通用引用

规则如下:

  • 如果 expr 是左值,那么 T 和 ParamType 都被推导成左值引用
  • 如果 expr 是右值,规则和 Case 1 一样。即忽略引用部分(&&),然后根据 expr 的类型来匹配 ParamType 和 T

Case 3: ParamType 既不是引用也不是指针

既然 ParamType 既不是引用也不是指针,那么用的就是传值方式了。

规则如下:

  1. 如果 expr 是引用,那么忽略引用部分
  2. 忽略 expr 的引用部分后,如果带 const/volatile 修饰符,那么也忽略掉
  3. 最后根据 expr 的类型来匹配 ParamType 和 T

举个例子:

template<class T>
void f(T param); // 模板函数

int x = 233;
const int &rx = x;
f(rx); // T 和 param 的类型为 int

这里的 rx 虽然为 const int & 类型,根据规则 1 和 2,忽略引用和 const,那么就变成了 int 类型。之所以要去掉 const,是因为 expr 不能被修改并不意味着其副本不能被修改。也就是说最后的 param 可以修改,因为修改它不会影响到 rx。

还有一种情况类似,需要注意。

const char *const ptr = "hello Netcan";

f(ptr); // T 和 param 的类型为 const char *

根据规则 2,去掉 const 限定符,那么变成的是 const char* 类型。可以看出去掉的是后面那个 const,因为 ptr 的值是 const,表明 ptr 不能被修改,传值的时候可以去掉;而前面那个 const 表面 ptr指向的内容 不能被修改,限定的不是 ptr,需要保留的。

数组作为参数

这种情况比较特殊,毕竟数组类型和指针不同,尽管有时候可以看成是同一个东西。之所以引起大多数人的疑惑,是因为在许多情况下,数组会退化 (decay) 成指针(指向它的第一个元素),这种退化才能允许如下代码通过:

const char name[] = "Netcan";
const char * ptrToName = name; // 这种情况就是数组退化成指针了

这里 ptrToName 是一个 const char* 指针,指向的是数组 name,而数组 name 的类型是 const char[7],显然const char*const char[7]并不是同一个类型,但是因为数组的退化规则,导致代码能编译通过。

如果数组传给一个模板(Case 3),情况如何?

f(name); // T 被推导成 const char*/const char[]

很显然,T 被推导成一个指针类型了,并不是真正的数组类型。但是如果声明成引用参数,如下:

template<class T>
void f(T& param); // 模板函数
f(name);

这里 T 被推导成真正的数组类型了,也就是说,T 被推导成const char[7],而 param 的类型为const char(&)[7]

根据这个技巧,可以写出如下模板函数,推导出数组的元素个数。

template<class T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept { // 因为我们关心的是数组元素个数,所以参数名可以省略了
	return N;
}

constexpr 关键字表明在编译过程中就能确定出结果,noexcept 有助于编译器优化。

书上推荐用内置的 std::array 类型作为数组使用。

函数作为参数

不仅仅是数组类型能退化成指针,函数类型也能退化成函数指针。这种情况和前面说的数组推导是一样的。这种情况在实践中用引用和指针没什么区别,不再概述,原理懂了就行。

Things to Remember

  • 模板推导中,argument 的引用性可以忽略
  • 当 param 是通用引用的时候,argument 为左值需要特别注意
  • 当 param 是传值类型 (Case 3) 的时候,argument 的 const/volatile 需要忽略
  • 模板推导中,argument 为数组或者函数的时候会退化成指针,除非 param 用引用

Item 2: Understand auto type deduction

auto 类型推导和模板推导差不多,但有些地方不同。之前说的模板参数推导:

template<class T>
void f(ParamType param); // 模板函数

f(expr); // 调用 f,argument 是 expr,根据 expr 来确定 T 和 param 的类型。

当用 auto 来声明变量的时候,auto 相当于这里面的 T,而变量类型相当于 ParamType。所以也有三种情况:

  • Case 1: 声明的类型是指针或者引用,但不是通用引用
  • Case 2: 声明的类型是是通用引用
  • Case 3: 声明的类型既不是引用也不是指针

按照模板推导的方法来做就是了。

但是有一点不同,需要特别指出。

C++98 有两种初始化变量的方法:

int x1 = 27;
int x2(27);

C++11 添加了 通用 初始化(就是花括号)变量的方法,如下:

int x3 = {27};
int x4{27};

后面 Item 5 会提到利用 auto 来代替直接指出变量类型的优点,所以将 int 用 auto 替代得到如下代码:

auto x1 = 27;
auto x2(27);
auto x3 = {27};
auto x4{27};

如上声明都能通过编译,但是有区别。前两条语句都是将 x 声明为 int 类型,而 后两条 语句声明的是 std::initializer_list<int> 类型,其包含了一个元素 27,这是一个巨大的陷阱,也是一些程序员只有在必须要用到花括号初始化的时候才用的原因。(需要注意的是,由于 C++ 标准委员会接受了提议 N3922,这个提议使得 x4 这种初始化不再为std::initializer_list 类型,而是 int 类型了。由于 N3922 不是 C++11 和 C++14 的部分,所以有些编译器实现了 N3922,比如说我用的clang 就是把 x4 推导为 int 类型。)

这就是 auto 类型推导需要 特别注意 的地方。当 auto 声明的变量初始化用花括号的时候,推导的类型为std::initializer_list。如果无法推导,那么报错。

而模板推导的时候,若传通用初始化,将会 报错 ,这点和 auto 不同,它不会推导成std::initializer_list 类型。

关于通用初始化,这两者表现出来的行为不同,作者也无法理解,就把它当做规则来记吧。

然而在 C++14 中,允许 auto 作为函数 返回类型 ,用来推导函数的返回类型,并且 C++14 也支持在 lambdas 中使用 auto 来声明param。这个时候 auto 使用的是 模板推导 的规则,而不是 auto 的推导规则了。也就是说如下代码无法编译通过:

auto createInitList() {
	return {1, 2, 3}; // 编译错误!无法推导出 {1, 2, 3} 的类型
}

std::vector<int> v;
auto resetV = [&v](const auto& newValue) { v = newValue; };

resetV({1, 2, 3}); // 编译错误!理由同上

Things to Remember

  • auto 类型推导通常和模板类型推导规则一样,但是 auto 类型推导将花括号初始化看做std::initializer_list,而模板类型推导不会
  • auto 用在函数返回值或者 lambdas 的 param 中,用的是模板推导的规则而不是 auto 类型推导规则。

Item 3: Understand decltype

decltype 关键字用来给出一个变量名或者表达式的 真正 类型。通常情况下使用没什么问题,这里不在举例。

在 C++11 中一个基本的用法就是用在声明函数的返回类型,而返回类型依赖于函数的 param 类型。

一般来说 operator[] 返回的通常是容器中元素的引用,即 T&。也有例外,比如std::vector<bool> 返回的不是bool&,而是一个新的临时对象,这点非常重要,在 Item 6 中会提到。

假如现在需要写一个函数,对用户进行权限检查,然后再索引容器中的元素,如下:

template<class Container, class Index>
?type? authAndAccess(Container& c, Index i) {
	autheticateUser();
	return c[i];
}

那么这个 ?type? 应该是什么类型呢?无从判断,这时候 decltype 派上用场了:

template<class Container, class Index>
auto authAndAccess(Container& c, Index i) -> decltype(c[i]) {
	autheticateUser();
	return c[i];
}

注意这里的 auto 并没别的作用,只是占位符罢了。而 param 表后面的 -> 是 C++11 的 trailing return type 语法,因为 decltype 需要用到前面声明的 c 和 i。

在 C++14 中可以省略后置返回类型写法,比如前面提到的,只用 auto 来推导函数的返回类型,将使用模板推导的规则:

template<class Container, class Index>
auto authAndAccess(Container& c, Index i) {
	autheticateUser();
	return c[i];
}

细心的话,会发现这样写有严重的 bug,那就是 丢失引用 。因为 c[i] 返回的是 T& 类型,而函数用的是auto,根据模板推导规则的 Case 3,会导致函数返回的类型为T,变成了右值。

上面这个例子就是想说明,我们需要 decltype 这样的类型推导,因为它能返回 精确 的类型。

C++ 预料到了这种情况,在 C++14 中提出了 decltype(auto) 说明符,很好地解决了这个问题,因此可以改写成这样:

template<class Container, class Index>
decltype(auto) authAndAccess(Container& c, Index i) {
	autheticateUser();
	return c[i];
}

这时候 authAndAccess 将会返回和 c[i]一样的类型了,比如 c[i]返回的是 T&,那么authAndAccess 也是 T&;c[i] 返回的是对象,那么 authAndAccess 也将返回对象。

同时也能用在初始化表达式中,如下:

Widget w;
const Widget &cw = w;
auto myWidget1 = cw; // myWidget1 的类型为 Widget
decltype(auto) myWidget2 = cw; // myWidget2 的类型为 const Widget &

authAndAccess还有一个问题,它的 argument 是一个左值引用,非 const,因为程序调用它可能需要修改检索到的值,这没什么问题。但是不能传一个右值 argument 给它,因为右值不能绑定到左值引用上。一个方案是重载authAndAccess,写一个左值和右值版本的,这样就需要维护两个版本了。因此可以用通用引用来实现,得出如下版本:

template<class Container, class Index>
decltype(auto) authAndAccess(Container&& c, Index i) {
	autheticateUser();
	return std::forward<Container>(c)[i];
}

decltype 也有例外情况,这里需要 注意 下。C++ 定义左值表达式 (x) 为一个左值,所以 decltype((x)) 的类型将是一个左值引用。也就是说:

int x = 2;
decltype((x)) y = x; // y 的类型为 int &

decltype(auto) f1() {
	int x = 0;
	return x; // 返回值为 int,因为 decltype(x)为 int
}

decltype(auto) f2() {
	int x = 0;
	return (x); // 返回值为 int&,因为 decltype((x))为 int&,返回一个局部变量的引用,非常危险
}

Things to Remember

  • decltype 大多数时候返回的类型和变量或者表达式的类型一致
  • 左值表达式 (x) 返回的是它的引用类型
  • C++14 支持 decltype(auto)说明符,虽然含有 auto,实际上用的是 decltype 的规则。

Item 4: Know how to view deduced types

这一节讲了如何查看 C++ 推导出来的类型。

IDE 编辑器

这个方法比较简单,需要 IDE 支持(IDE 从编译器那里取得相关信息),比如将鼠标放到变量上,就会显示出该变量的类型了。

但是也有缺点,就是当涉及到比较复杂的类型,那么给出来的信息就没什么用了。

编译器诊断

更好的办法是用编译器编译的时候给出类型信息,就是让编译器报错,从而给出相关信息。

所以可以这么做,声明一个未定义的模板:

template<class T>
class TD;

当定义一个 TD 对象的时候,由于 TD 未定义,所以会报错。比如我想知道 x 的类型,那么:

TD<decltype(x)> xType;

编译得出如下信息:

error: implicit instantiation of undefined template 'TD<const int *>'
    TD<decltype(x)> xType;

很容易看出,x 的类型为const int *

运行时输出

如果想让程序运行过程中,给出类型信息,要怎么做呢?书上首先介绍了 typeid 操作符,typeid会返回一个 std::type_info 对象,它有一个成员函数name,可以返回类型的字符串形式(const char*)。

cout << typeid(x).name << endl;

输出的结果为 PKiPK 表示 const *i 表示 int。可以用c++-filt 这个工具来获得可读的信息:

$ ./a.out | c++filt -t
  int const*

但是 typeid 也有巨坑,这里给出一个比较复杂的例子:

template<class T>
void f(const T& param);

const auto vw = vector<Widget>(...); // ... 省略初始化的元素了
f(&vw[0]);

那么这个 T 和 param 的类型是什么呢?先来分析下,vwconst vector<Widget> 类型,那么 vw[0] 就是 const Widget & 类型,&vw[0]const Widget * 类型。然后根据 param 匹配,很容易知道 param 的类型为 const Widget * const & 了,从而得知 T 的类型为const Widget *

那么看看 typeid 给出来的结果:

template<class T>
void f(const T& param) {
	using std::cout;
	cout << "T =" << typeid(T).name() << endl;
	cout << "param =" << typeid(param).name() << endl;
}

编译运行:

$ ./a.out | c++-filt -t
  T = Widget const*
  param = Widget const*

可以看出 T 的类型对的,而 param 的类型是错的!再说了 T 怎么可能和 param 同一个类型,这也说明了 std::type_info::name 给出来的信息不可靠,因为 typeid 是通过传值方式(即 Item 1 的 Case 3)来处理参数,将 param 的引用和 const 忽略后,从而变成T param,这也就是为啥 T 和 param 的类型一样了。

具体来说:

printf("%d\n", typeid(const int) == typeid(int)); // true
printf("%d\n", typeid(int &) == typeid(int)); // true

如果想得到正确的信息,最后给出的解决方案是用Boost.TypeIndex

#include <boost/type_index.hpp>
template<class T>
void f(const T& param) {
	using std::cout;
	using boost::typeindex::type_id_with_cvr;

	cout << "T ="
		<< type_id_with_cvr<T>().pretty_name()
		<< endl;
	cout << "param ="
		<< type_id_with_cvr<decltype(param)>().pretty_name()
		<< endl;
}

得出如下结果:

T = Widget const*
param = Widget const* const&

顾名思义,with_cvr就表示保留 const/volatile 和引用了。

Things to Remember

  • 查看推导的类型可以通过 IDE,编译器错误信息,Boost.TypeIndex库来实现
  • 最重要的是理解 C++ 的推导规则
支持一下
扫一扫,支持Netcan
  • 微信扫一扫
  • 支付宝扫一扫