0%

Effective Modern C++ 笔记 Chapter1 类型推断 (Item 1-4)

C++98只有一组类型推断的规则:函数模板。C++11增加了两组:autodecltype。C++14则扩展了autodecltype的适用范围。

我们需要理解类型推断的规则,避免出了问题后脑海中一片白茫茫,Google都不知道该搜什么东西。

Item1: 理解模板类型推断

应用于函数模板的那套类型推断的规则很成功,数以百万的开发者都在用,即使大多数人都说不清楚这些规则。

但理解类型推断的规则非常重要,因为它是auto基础。但auto的有些规则反直觉,因此要好好学习下面的规则。

一个函数模板的例子:

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

以及对它的调用:

1
f(expr);

expr要用来推断两个类型:TParamTypeParamType通常与T不同,比如带个const&

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

如果调用是:

1
2
int x = 0;
f(x); // ParamType is const T&

那么T被推断为int,而ParamType则是const int&

这里T就是expr的类型,符合大家的期望。但不总是这样。T的类型不仅与expr有关,也与ParamType的形式有关:

  • ParamType是指针或引用(不是universal reference即普适引用)。
  • ParamType是普适引用。
  • ParamType不是指针也不是引用。

Case1: ParamType是引用或指针,但不是普适引用

规则:

  1. 如果expr是引用,就忽略引用。
  2. 再用ParamType去模式匹配expr,来确定T

例如:

1
2
3
4
5
6
7
8
9
10
template <typename T>
void f(T& param); // param is a reference

int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int

f(x); // T is int, param is int&
f(cx); // T is const int, param is const int&
f(rx); // T is const int, param is const int&

注意cxrx都令T被推断为const intParamTypeconst int&。这说明把const对象传给T&参数是安全的,不会令const对象被修改。

第三个调用中rx是引用,但T不是,是因为rx的引用属性在推断中被忽略了。

ParamType如果是const T&,那么三个调用中T都会被推断为int,而不再保留exprconst了。

如果ParamType是指针,那么类似:

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

int x = 27;
const int* px = &x;

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

总结起来就是,如果ParamType是引用或指针:

  1. expr的引用或指针性会被忽略。
  2. 如果exprconst,那么TParamType中需要且只需要有一个带const,来保证exprconst

Case2: ParamType是普适引用

如果ParamTypeT&&(即普适引用),但expr是左值引用,那么规则比较复杂:

  • 如果expr是左值,那么TParamType都被推断为左值引用。两个不寻常处:
    1. 这是模板类型推断中唯一的T被推断为引用的场景。
    2. 如果expr是右值,那么同Case1。

例如:

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
void f(T&& param);

int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int

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

Case3: ParamType既不是指针也不是引用

如果ParamType既不是指针也不是引用,那么f就是传值调用,那么param就是一个全新的对象,规则:

  1. 同上,如果expr是引用,就忽略引用性。
  2. 如果exprconstvolatile,也忽略。

例子:

1
2
3
4
5
6
7
8
9
10
template <typename T>
void f(T param);

int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int

f(x); // T and param are both int
f(cx); // T and param are both int
f(rx); // T and param are both int

注意expr如果是指向const对象的指针,那么这个const不能被忽略掉。

数组参数

注意:数组作为参数时与指针的行为不同。

传值调用中,数组会被推断为指针:

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

const char name[] = "J. P. Briggs"; // name is const char[13]

f(name); // T is const char*

但如果传引用,数组会被推断为数组:

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

f(name); // T is const char [13] and ParamType is const char (&)[13]

我们可以利用这点在编译期拿到一个数组的长度:

1
2
3
4
5
template <typename T, size_t N>
constexpr size_t arraySize(T (&)[N]) noexcept
{
return N;
}

所以我们可以这么定义数组:

1
2
int keyVals[] = {1, 3, 7, 9, 11, 22, 35}; // keyVals has 7 elements
int mappedVals[arraySize(keyVals)]; // so does mappedVals

或者用更现代的方式:

1
std::array<int, arraySize(keyVals)> mappedVals;

函数参数

另一个会退化为指针的类型是函数。函数会退化为函数指针,且规则与数组相同。例如:

1
2
3
4
5
6
7
8
9
10
void someFunc(int, double);    // someFunc's type is void(int, double)

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

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

f1(someFunc); // T and ParamType are both void (*)(int, double)
f2(someFunc); // T is void(int, double), ParamType is void (&)(int, double)

当然在实践上函数和函数指针几乎没有区别。

Item2: 理解auto类型推断规则

auto使用的类型推断规则与模板的规则几乎一样。

1
2
3
auto x = 27;
const auto cx = x;
const auto& rx = x;

auto就相当于上节中的T,而xcxrx的类型则是ParamType

回忆一下上节介绍的ParamType的三种情况,同样可以应用在auto上:

  • Case1: auto类型是指针或引用,但不是普适引用。
  • Case2: auto类型是普适引用。
  • Case3: auto类型既不是指针也不是引用。
1
2
3
4
5
6
auto x = 27;          // case3: int
const auto cx = x; // case3: const int
const auto&& rx = x; // case1: const int&&
auto&& uref1 = x; // case2: int&. x is lvalue
auto&& uref2 = cx; // case2: const int&. cx is lvalue
auto&& uref3 = 27; // case2: int&&. 27 is rvalue

以及针对数组和函数的规则:

1
2
3
4
5
6
7
8
9
const char name[] = "R. N. Briggs";    // name is const char[13]

auto arr1 = name; // arr1 is const char*
auto& arr2 = name; // arr2 is const char(&)[13]

void someFunc(int, double); // someFunc is void(int, double)

auto func1 = someFunc; // func1 is void (*)(int, double)
auto& func2 = someFunc; // func2 is void (&)(int, double)

例外:auto会把所有的统一初始化式(花括号初始化式)当作std::initializer_list<T>对待。

int的初始化中:

1
2
3
4
int x1 = 27;
int x2(27);
int x3 = {27};
int x4{27};

以上四种形式得到的x1x4都是一个值为27int

但如果换成auto,后两者的类型就有些出乎意料了:

1
2
3
4
auto x1 = 27;      // x1 is int
auto x2(27); // ditto
auto x3 = {27}; // x3 is std::initializer_list<int>
auto x4{27}; // ditto

即使这个初始化式根本没办法匹配成std::initializer_list<T>

1
auto x5 = {1, 2, 3.0}; // error! 

如果把std::initializer_list<T>传给一个函数模板,行为则不一样:

  • 如果param的类型为T,则报错:

    1
    2
    3
    template <typename T>
    void f(T param);
    f({11, 23, 9}); // error! can't deduce T
  • 如果param的类型为std::initializer_list<T>,则可以:

    1
    2
    3
    template <typename T>
    void f(std::initializer_list<T> param);
    f({11, 23, 9}); // T is int and param is std::initializer_list<int>

C++14允许auto作为函数的返回类型,及lambda函数的参数类型,但这两种情况下的auto实际应用的是模板的类型推断规则,而不是上面说的auto规则!

1
2
3
4
5
6
7
8
9
10
auto createInitList()
{
return {1, 2, 3}; // error! can't deduce type for {1, 2, 3}
}

std::vector<int> v;
...
auto resetV = [&v](const auto& newValue) { v = newValue; }
...
resetV({1, 2, 3}); // error! can't deduce type for {1, 2, 3}

Item3: 理解decltype

通常decltype返回表达式的精确类型(注意,与模板和auto不同):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const int i = 0;             // decltype(i) is const int
bool f(const Widget& w); // decltype(w) is const Widget&
// decltype(f) is bool(const Widget&)
struct Point {
int x, y; // decltype(Point::x) is int
}; // decltype(Point::y) is int
Widget w; // decltype(w) is Widget
if (f(w)) ... // decltype(f(w)) is bool
template <typename T>
class vector {
public:
...
T& operator[](std::size_t index);
...
};
vector<int> v; // decltype(v) is vector<int>
...
if (v[0] == 0) ... // decltype(v[0]) is int&

C++11中,decltype还可以用来表示一个需要推断出来的类型返回类型:

1
2
3
4
5
template <typename Container, typename Index>   // requires refinement
auto authAndAccess(Container& c, Index i) -> decltype(c[i]) {
authenticateUser();
return c[i];
}

一般容器的operator[]都会返回T&,但像vector<bool>就会返回一个中间类型,而用上面的decltype就能处理这种情况。

C++14中我们可以省掉尾部的返回类型:

1
2
3
4
5
template <typename Container, typename Index>   // C++14, not quite correct
auto authAndAccess(Container& c, Index i) {
authenticateUser();
return c[i];
}

上面的形式的问题在于:auto会抹去类型中的引用,导致我们想返回T&,但实际却返回了T

所以实际上C++14我们需要写成decltype(auto)

1
2
3
4
5
template <typename Container, typename Index>   // C++14, requires refinement
decltype(auto) authAndAccess(Container& c, Index i) {
authenticateUser();
return c[i];
}

我们可以把decltype(auto)中的auto替换成return后面的表达式,即decltype(c[i]),这样就能精确的表达我们的意图了。

decltype(auto)不仅能用于模板,同样能用于想拿到表达式精确类型的场合:

1
2
3
4
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // myWidget1 is Widget
decltype(auto) myWidget2 = cw; // myWidget2 is const Widget&

再回头看上面C++14版本的authAndAccess,它的问题是参数类型为左值引用,因此无法接受右值参数(const Container&可以,但返回类型带const)。

什么情况下我们会传入一个容器的右值引用?也许是想拿到一个临时容器的成员的拷贝吧。

我们当然可以用重载来实现同时支持左值和右值引用的目的。如果不用重载的话,就需要把参数类型改成普适引用了:Container&& c。为了能同时处理左值和右值引用,我们要引入std::forward<T>,它在传入右值时返回右值,传入左值时返回左值。

1
2
3
4
5
template <typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i) {
autherticateUser();
return std::forward<Container>(c)[i];
}

C++11中我们需要使用尾部返回类型:

1
2
3
4
5
6
7
template <typename Container, typename Index>
auto authAndAccess(Container&& c, Index i)
-> decltype(std::forward<Container>(c)[i])
{
autherticateUser();
return std::forward<Container>(c)[i];
}

decltype一个容易忽视的特性是:如果它的括号中的表达式是左值,且不是一个变量的名字,那么decltype会保留引用。

因此decltype(x)返回int,但decltype((x))返回int&

因此在使用decltype(auto)时,改变return后面的表达式形式,可能会改变返回的类型:

1
2
3
4
5
6
7
8
9
10
decltype(auto) f1() {
int x = 0;
...
return x; // return int
}
decltype(auto) f2() {
int x = 0;
...
return (x); // return int& to a local variable!
}

Item4: 如何显示推断出来的类型

IDE

编译器诊断消息

可以通过编译器在出错时给出的诊断消息来显示推断出的类型。

首先声明一个模板类,但不给出任何定义:

1
2
template <typename T>
class TD;

然后用你想显示的类型来实例化这个模板:

1
2
TD<decltype(x)> xType;
TD<decltype(y)> yType;

显然编译会出错,错误消息中就有我们想看到的类型:

1
2
error: aggregate 'TD<int> xType' has incomplete type and cannot be defined
error: aggregate 'TD<const int *> yType' has incomplete type and cannot be defined

不同的编译器可能会给出不同格式的诊断消息,但基本上都是能帮到我们的。

运行时输出

方法1: std::type_info::name

1
2
std::cout << typeid(x).name() << std::endl;
std::cout << typeid(y).name() << std::endl;

这种方法输出的类型可能不太好懂,比如在GNU和Clang中int会缩写为i,而const int*会缩写为PKi

但有的时候std::type_info::name的输出靠不住:

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
void f(const T& param) {
std::cout << typeid(T).name() << std::endl;
std::cout << typeid(param).name() << std::endl;
}

std::vector<Widget> createVec();
const auto vw = createVec();

if (!vw.empty()) {
f(&vw[0]);
}

在GNU下输出是:

1
2
PK6Widget
PK6Widget

意思是const Widget*

问题来了:根据Item1的规则,这里T应该是const Widget*,而param应该是const Widget* const&啊!

不幸的是,这就是std::type_info::name的要求:它要像传值调用一个模板函数一样对待类型,即“去掉引用、const、volatile”。

所以const Widget* const&最终变成了const Widget*

方法2: boost::typeindex::type_id_with_cvr

在方法1不奏效的时候,可以用boost::typeindex::type_id_with_cvr来查看类型。

1
2
3
4
5
6
7
#include <boost/type_index.hpp>

template <typename T>
void f(const T& param) {
std::cout << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
std::cout << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name() << std::endl;
}

GNU输出为:

1
2
Widget const*
Widget const* const&

而且,因为Boost是一个跨平台的开源库,因此在各个平台的各个编译器下,我们都能得到可用且正确的信息,尽管输出不完全一致。

目录