EffectiveModernCpp(1):类型推导
1. 类型推导
考虑这样一个模版:
template<typename T>
void f(ParamType param);
f(expr);
编译器会使用 $expr$ 进行类型推导,一个是针对 $T$ 的,另一个是针对 $ParamType$ 的。这两个类型通常是不同的。例如:
template<typename T>
void f(const T ¶m);
int x = 0;
f(x);
这时 $T$ 被推导为 $int$ ,$ParamType$ 被推导为 $const$ $int$ & 。事实上,$T$ 的推导不仅取决于 $expr$ ,还取决于 $ParamType$ 。这里有三种情况:
- $ParamType$ 是一个指针或引用,但不是通用引用;
- $ParamType$ 是一个通用引用;
- $ParamType$ 既不是指针也不是引用。
1.1 情况一
最简单的情况是 $ParamType$ 是一个指针或引用但不是普通引用,这时类型推导过程为:
- 如果 $expr$ 的类型是引用,忽略引用部分;
- 根据剩下部分推导 $T$ ,并得出 $ParamType$ 。
template<typename T>
void f(T ¶m);
int x = 27;
const int cx = x;
const int &rx = cx;
f(x); // T为int, ParamType为int &
f(cx); // T为const int, ParamType为const int &
f(rx); // T为const int, ParamType为const int &
注意第三个例子中,即使 $rx$ 是一个引用,$T$ 也会被推导为非引用。
1.2 情况二
如果 $ParamType$ 是一个通用引用,那么推导过程如下:
- 如果 $expr$ 是左值,$T$ 和 $ParamType$ 都会被推导为左值引用;
- 如果 $expr$ 是右值,那么适用上一个情况的规则。
template<typename T>
void f(T &¶m);
int x = 27;
const int cx = x;
const int &rx = cx;
f(x); // T为int &, paramType为int &
f(cx); // T为const int &, paramType为const int &
f(rx); // T为const int &, paramType为const int &
f(27); // T为int, paramType为int &&
1.3 情况三
当 $ParamType$ 既不是指针也不是引用时,采用传值方式。推导过程如下:
- 如果 $expr$ 是引用,忽略引用部分;
- 如果 $expr$ 是 $const$ ,忽略 $const$ ;
- 如果 $expr$ 是 $volatile$ ,忽略 $volatile$ 。
template<typename T>
void f(T param);
int x = 27;
const int cx = x;
const int &rx = x;
const char *const ptr = "pointers";
f(x); // T和ParamType都是int
f(cx); // T和ParamType都是int
f(rx); // T和ParamType都是int
f(ptr); // T和ParamType都是const char *
最后一个例子中,由于 $const$ 被忽略,所以类型会被推导为 $const$ $char$ $\star$ 。
1.4 数组
const char name[] = "name"; // 类型为const char[13]
const char *ptrToName = name;
虽然 $name$ 和 $ptrToName$ 的类型不同,但是C++
允许数组退化为一个指针。应用在函数上,体现为:
void myFunc(int param[]);
void myFunc(int *param);
上面这两个函数是等价的。将这个规则应用于模版:
template<typename T>
void f(T param);
f(name); // T为const char *
虽然函数不能接收数组,但是可以接收数组引用。
template<typename T>
void f(T ¶m);
f(name); // T为const char[13], paramType为const char (&)[13]
从而我们可以通过模版函数推导出数组大小:
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept {
return N;
}
1.5 函数
void foo(int, double);
template<typename T>
void f1(T param);
template<typename T>
void f2(T ¶m);
f1(foo); // T和paramType为void (*)(int, double)
f2(foo); // T和paramType为void (&)(int, double)
与数组相同,函数也会退化为指针,但是对于引用,它们不会退化。
2. auto
auto x = 27; // int
const auto cx = x; // int
const auto &rx = cx; // int
const char name[] = "name";
auto arr1 = name; // const char *
auto &arr2 = name; // const char (&)[13]
void foo(int, double);
auto f1 = foo; // void (*)(int, double)
auto f2 = foo; // void (&)(int, double)
$auto$ 类型推导除了一个例外,其他情况都和模版类型推导一样。C++
中允许以下的类型声明方式:
auto x1 = 27; // int
auto x2(27); // int
auto x3 = {27}; // std::initializer_list
auto x4{27}; // std::initializer_list
这就是 $auto$ 推导不同于模版推导的地方,使用花括号的变量声明会被推导为 $initializer_-list$ ,后者是一个模版。$initializer_-list$ 在实例化的过程中也要被推导,推导出的类型为 $initalizer_-list$<$int$> 。
3. decltype
const int i = 0; // decltype(i)为const int
bool f(const Widget &w); // decltype(f)是bool (const Widget &)
struct Point {
int x; // decltype(Point::x)为int
int y; // decltype(Point::y)为int
};
std::vector<int> v; // decltype(v)为std::vector<int>
v[0] = 0; // decltype(v[0])为int &
$decltype$ 会返回精确的结果,主要用途是作为模版函数的返回类型,根据不同的形参返回不同的类型。
template<typename Container, typename Index>
auto authAndAccess(Container &c, Index i) -> decltype(c[i]) {
authenticateUser();
return c[i];
}
函数名称前的 $auto$ 不会做任何推导工作,只是暗示使用尾置返回类型。C++11
允许自动推导单一语句的lambda
表达式返回类型,C++14
扩展到允许自动推导所有lambda
表达式和函数,甚至包含多条语句。
template<typename Container, typename Index> // C++14
auto authAndAccess(Container &c, Index i) {
authenticateUser();
return c[i];
}
由于 $auto$ 应用于函数时使用的是模版推导的方式,所以根据我们在情况一中所说的,如果表达式是一个引用,那么引用会被忽略,从而以下代码不合法:
std::deque<int> d;
authAndAccess(d, 5) = 10; // 编译失败
为了避免这种情况,我们应该使用 $decltype$ ,因为 $decltype$ 会返回精确类型,包括引用。
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container &c, Index, i) {
authenticateUser();
return c[i];
}
$decltype(auto)$ 是C++14
引入的,允许我们通过 $decltype$ 方式推导返回值,而不是使用模版推导方式。在使用 $decltype(auto)$ 之后,函数会返回引用类型。
$decltype(auto) $ 也不局限于函数返回类型:
Widget w;
const Widget &cw = w;
auto myWidget1 = cw; // Widget
decltype(auto) myWidget2 = cw; // const Widget &
向 $authAndAccess$ 传递一个右值是不合法的,因为右值不能绑定到左值引用上。如果想要它支持左值和右值,需要修改为通用引用:
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container &&c, Index i) {
authenticateUser();
return std::forward<Container>(c)[i];
}
$decltype$ 也存在一些特殊情况。
int x = 0; // decltype(x)为int
// decltype((x))为int&
在上述例子中,$decltype(int)$ 为 $int$ ,这没有问题,但是 $decltype((int))$ 却变成了 $int$ & 。对于 $x$ 来说,它是一个左值,而C++11
定义了 $(x)$ 也是一个左值,并且对后者的 $decltype$ 调用会返回引用类型。将这个特性应用在函数上,结果如下:
decltype(auto) f1() {
int x = 0;
return x; // int
}
decltype(auto) f2() {
int x = 0;
return (x); // int &
}
4. 类型诊断
为了获取真实的推导结果,我们需要采取一些诊断方式。
4.1 编译器诊断
编译出错时,编译器会输出报错信息,这些信息中会包含推导结果。
// 声明一个模版类但不定义
template<typename T>
class TD;
我们声明了一个模版类但不定义,从而在尝试实例化这个类的时候就会报错。
int x = 1;
const int *y = &x;
TD<decltype(x)> xType;
TD<decltype(y)> yType;
上面的代码产生类似于下面的错误:
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
4.2 运行时输出
标准I/O
提供了一种格式化输出的方法:
std::cout << typeid(x).name() << std::endl;
std::cout << typeid(y).name() << std::endl;
这种方法会产生一个 $std::type_-info$ 对象,并调用该对象的 $name$ 函数。要注意的是,这个函数不保证返回有意义的东西,比如GNU
和Clang
可能会返回 $i$ ( 表示 $int$ ),和 $PKi$ ( 表示 $pointer$ $to$ $\require{cancel}\bcancel{konst}$ $const$ $int$ )。
考虑一个更复杂的例子:
template<typename T>
void f(const T ¶m) {
std::cout << "T=" << typeid(T).name() << "\n"
<< "param=" << typeid(param).name() << std::endl;
}
std::vector<Widget> createVec();
const auto vw = createVec();
// ...
if (!vw.empty()) {
f(&vw[0]);
}
GNU
和Clang
会输出类似如下的结果:
T=PK6Widget
param=PK6Widget
数字 $6$ 是类名称的字符串长度。看起来这种输出方式好像可以理解,但是推导一下却发现不是这样的,因为 $T$ 和 $param$ 的类型输出一致,很明显这是错的。所以 $std::type_-info::name$ 的结果并不总是可信。相比于标准库,Boost
库是更好的选择:
#include <boost/type_index.hpp>
template<typename T>
void f(const T ¶m) {
using std::cout;
using boost::type_index::type_id_with_cvr;
cout << "T=" << type_id_with_cvr<T>().pretty_name() << "\n"
<< "param=" << type_id_with_cvr(decltype(param)>().pretty_name()
<< std::endl;
}
在GNU
和Clang
环境下,会输出:
T=Widget const *
param=Widget const * const&
5. 再谈auto
$auto$ 的概念很简单,但是如果使用不小心,会产生一些错误。以下列举了一些 $auto$ 的使用法则。
5.1 优先使用auto
C++
的变量声明是不会将变量清空的,这意味着当你使用:
int x; // 声明但不初始化
的时候,$x$ 的值是完全不确定的。使用 $auto$ 则可以避免这个问题,因为你不初始化便无法推导出结果。
此外,当有些变量名过于冗长时:
template<typename T>
void dwim(It b, It e) {
while (b != e) {
typename std::iterator_traits<It>::value_type
currValue = *b;
}
}
我们也可以使用 $auto$ 简化。因为 $auto$ 使用类型推导技术,所以它还可表达一些只有编译器才能知道的类型:
auto derefUPLess =
[](const std::unique_ptr<Widget> &p1,
const std::unique_ptr<Widget> &p2) {
return *p1 < *p2;
};
如果使用C++14
,上述代码还可以继续简化:
auto derefUPLess =
[](const auto &p1, const auto &p2) {
return *p1 < *p2;
};
$auto$ 还可以避免一个问题,称之为类型快捷方式 ( $type$ $shortcuts$ ) 问题。
std::vector<int> v;
unsigned sz = v.size();
$std::vector::size$ 的返回类型为 $std::vector$<$int$>$::size_-type$ ,后者实际上也是无符号整型。但是这会造成移植性问题,比如在 $64$ 位Windows
系统中,前者为 $64$ 位,后者为 $32$ 位。通过使用 $auto$ ,我们可以避免这个问题。
std::unordered_map<std::string, int> m;
for (const std::pair<std::string, int> &p : m) {
// ...
}
上述代码好像没有问题,而且也能正常运行,但是还是有问题。$std::unordered_-map$ 的 $key$ 是一个常量,但是 $for$ 循环中声明的不是常量。从而编译器需要在每个遍历过程中不断创建临时对象,将该临时对象引用绑定到 $p$ ,并在每次迭代结束后销毁这个临时对象。同样,使用 $auto$ 也可以避免这个问题。
5.2 通过显式类型避免错误推导
std::vector<bool> features(const Widget &w);
auto highPriority = features(w)[5]; // 应该使用bool显式声明
processWidget(w, highPriority); // 未定义行为
$processWidget$ 是一个未定义行为,因为 $std::vector$<$bool$>$::operator[\ ]$ 不会返回容器元素的引用,而是返回一个 $std::vecotr$<$bool$>$::reference$ 对象,从而 $auto$ 不会推导为 $bool$ 。虽然如此,但是 $reference$ 是可以隐式转换为 $bool$ 的,为什么这里还是未定义行为呢?因为 $reference$ 的行为依赖于具体实现,举例来讲,其中一种实现是包含一个指向结果的指针。这时,我们调用 $features$ ,后者返回一个临时 $vector$ 对象,$reference$ 的成员指针指向这个临时对象中的某个元素。之后临时对象销毁,从而导致 $reference$ 的成员指针变为了悬垂指针。
$std::vector$<$bool$>$::reference$ 是代理类的一个应用,一些代理类被设计为对客户可见,比如 $std::shared_-ptr$ 和 $std::unique_-ptr$ ,其他代理类则与之相反。作为一个通则,不可见的代理类不应该使用 $auto$ 。因为这样类型的对象的生命周期通常被设计为不超过一条语句,使用 $auto$ 违反了它们的设计理念。
但是实际上,开发者并不知道哪些函数返回的是代理类,往往都是在跟踪一些问题时才能发现代理类。$auto$ 本身没问题,问题是 $auto$ 不会推导出你想要的类型。解决方案是使用另一种类型推导形式,称为显式类型初始化惯用法 ( $the$ $explicitly$ $typed$ $initialized$ $idiom$ )。
auto highPriority = static_cast<bool>(features(w)[5]);