Effective Modern C++ 笔记
类型推导
item1 理解模版类型推导
- 在模板类型推导时,有引用的实参会被视为无引用,他们的引用会被忽略
- 对于通用引用的推导,左值实参会被特殊对待
- 对于传值类型推导,
const
和/或volatile
实参会被认为是non-const
的和non-volatile
的- 在模板类型推导时,数组名或者函数名实参会退化为指针,除非它们被用于初始化引用
考虑伪代码的函数模板:
template<typename T>
void f(ParamType param);
// 调用
f(expr);
在编译期间使用 expr 进行两个类型推导:一个是针对 T 的,一个是针对 ParamType,后者通常是包含修饰的 T。
情景一:ParamType 是一个指针或引用,但不是通用引用
template<typename T> void f(T& param); int x = 27; // f(x) T: int; param: int& const int cx = x; // f(cx) T: const int; param: const int& const int& rx = x; // f(rx) T: const int; param: const int& // 即使 rx 是个引用,但 T 在推导中忽略引用性,右值引用同理 template<typename T> void f(const T& param); // reference-to-const int x = 27; // f(x) T: int; param: const int& const int cx = x; // f(cx) T: int; param: const int& const int& rx = x; // f(rx) T: int; param: const int& // 即使 rx 是个引用,但 T 在推导中忽略引用性,右值引用同理 template<typename T> void f(T* param); int x = 27; // f(&x) T: int; param: int* const int *px = &x; // f(px) T: const int; param: const int*
情景二:ParamType 是一个通用引用(通用引用,模板 T,声明形式为 T&&)
template<typename T> void f(T&& param); // 通用引用考虑传参的参数是左值还是右值 // - 左值:那么 T 和 param 都会被推导为左值引用 // - 右值:那么 T 会按照情景一进行推导,param 为相应的右值引用 int x = 27; // f(x) T: int&; param: int& const int cx = x; // f(cx) T: const int&; param: const int& const int &rx = cx; // f(rx) T: const int&; param: const int& // f(27) T: int; param: int&&
情景三:ParamType 既不是指针也不是引用(传值)
template<typename T> void f(T param); // param 传参为拷贝新对象,既然是新对象,则引用、顶层const、volatile 都会被忽略 int x = 27; const int cx = x; const int& rx = cx; // T 和 param 的类型都是 int
数组实参与函数实参:数组和函数在传参时会退化成指针,但在接收引用 param 时可以接收指向数组的引用。
template<typename T> void f(T& param); int arr[] = {1, 2, 3}; // f(arr) T: int[3], param int (&)[3] // 通过接收数组的引用,还能创建模板函数来推导数组的大小 template<typename T, std::size_t N> constexpr std::size_t arraySize(T (&)[N]) noexcept { return N; }
item2 理解 auto 类型推导
auto
类型推导通常和模板类型推导相同,但是auto
类型推导假定花括号初始化代表std::initializer_list
,而模板类型推导不这样做- 在C++14中
auto
允许出现在函数返回值或者lambda函数形参中,但是它的工作机制是模板类型推导那一套方案,而不是auto
类型推导
auto 推导与 item1 模板推导大部分一致,三个情景稍微修改就能使用 auto
- 情景一:类型说明符是一个指针或引用但不是通用引用
- 情景二:类型说明符一个通用引用
- 情景三:类型说明符既不是指针也不是引用
但在有初始化列表时表现不一致
auto x = {1, 2, 3}; // x 的类型为 std::initializer_list<int> template<typename T> void f(T param); // 等价形式无法推导 f({1, 2, 3}) // 如果模板需要推导此类需要 template<typename T> void f(std::initializer_list<T> initList); // f({1, 2, 3}) T 为 int,initList 为 std::initializer_list<int> // 对于 lambda 中的 auto 推导在实际上是模板推导那套而不是 auto 类型 auto createInitList() { return { 1, 2, 3 }; //错误!不能推导{ 1, 2, 3 }的类型 } std::vector<int> v; // … auto resetV = [&v](const auto& newValue){ v = newValue; }; //C++14 … resetV({ 1, 2, 3 }); //错误!不能推导{ 1, 2, 3 }的类型
item3 理解 decltype
decltype
总是不加修改的产生变量或者表达式的类型。- 对于
T
类型的不是单纯的变量名的左值表达式,decltype
总是产出T
的引用即T&
。- C++14支持
decltype(auto)
,就像auto
一样,推导出类型,但是它使用decltype
的规则进行推导。
decltype 相比 auto 或者模板推导,它只是简单的返回名字或者表达式的类型。
用一个完善的例子理解 lambda 中的 decltype:
template<typename Container, typename Index> //最终的C++14版本 decltype(auto) // decltype(auto)理解: auto 代表返回值需要推导,decltype 代表按照 decltype 的方式来推导返回值 authAndAccess(Container&& c, Index i) // 通用引用可以传递右值 { authenticateUser(); return std::forward<Container>(c)[i]; // forward 实现完美转发 }
C++11 需要显示指定函数返回类型:
template<typename Container, typename Index> //最终的C++11版本 auto authAndAccess(Container&& c, Index i) ->decltype(std::forward<Container>(c)[i]) { authenticateUser(); return std::forward<Container>(c)[i]; }
如果一个不是单纯变量名的左值表达式的类型是
T
,那么decltype
会把这个表达式的类型报告为T&
。decltype(auto) f1() { int x = 0; … return x; //decltype(x)是int,所以f1返回int } decltype(auto) f2() { int x = 0; return (x); //decltype((x))是int&,所以f2返回int& UB!! }
item4 学会查看类型推导结果
- 类型推断可以从IDE看出,从编译器报错看出,从Boost TypeIndex库的使用看出
- 这些工具可能既不准确也无帮助,所以理解C++类型推导规则才是最重要的
Boost TypeIndex 库
#include <boost/type_index.hpp>
template<typename T>
void f(const T& param)
{
using std::cout;
using boost::typeindex::type_id_with_cvr;
//显示T
cout << "T = "
<< type_id_with_cvr<T>().pretty_name()
<< '\n';
//显示param类型
cout << "param = "
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
}
auto
item5 优先考虑 auto 而非显式类型声明
用 auto
的几个好处:
auto 变量必须要初始化,所以避免了忘记初始化带来的 ub
闭包的类型只有编译器知道,而 auto 能够表示。在 C++14 中,形参也可以使用 auto。
- 如果不使用
auto
,std::function
确实能够将闭包存放在该对象中,但用此方法语法冗长、重复写形参、实例化的 function 对象额外空间,有可能还会在堆上分配空间
- 如果不使用
可以避免一些可移植性问题,例如
std::vector<int>::size_type
在 win32 是 32 位,在 win64 是 64 位在某些情况更有效率。考虑哈希表的 range-base 的遍历
std::unordered_map<std::string, int> m; for (const std::pair<std::string, int>& p: m) { // do something // 实际上 m 里的 pair 是 std::pair<const std::string, int> 的 // 编译器会拷贝一个临时对象使得这个语法有效 // 即类型不匹配会降低效率 } // 而 auto 避免了此问题 for (const auto& p: m) { // do something }
当然,auto 是可选项,在 item2 和 6 中展示了有些坑。如果认为 auto 影响可读性,那可以好好配置一下 IDE。
item6 auto 推导不顺心则使用显式类型
auto 在某些时候会失去效用,例如隐式的代理类,以及想要的隐式转换。
隐式代理类的错误推导:以
std::vector<bool>::operator[]
为例,C++ 为其实现了一个特化版本,因为内存是按字节寻址的,bool 不需要一个字节存储,所以底层实质上是位图,那么operator[]
自然而然不是返回一个引用,而是使用了一个代理类来实现类似其他std::vector::operator[]
的功能。具体而言,代理类存储了一个原序列的指针和一个掩码,在调用
operator[]
时生成一个代理类对象和其掩码,代理类实现了operator bool()
的函数,即static<bool>(xxx)
那么考虑下列代码,
std::vector<bool> features(const Widget& w);
bool bit = features(w)[5]; // 类型为 std::vector<bool> 的临时对象 (features(w))[5] => reference(temp, mask(5)) => static_cast<bool>(reference)
auto bit_ref = features(w)[5]; // 类型为 std::vector<bool> 的临时对象 (features(w))[5] => reference(temp, mask(5))
// 这里 bit_ref 类型为 bit_reference,其中的 temp 指针和 mask 被拷贝至此
bool bit_ = static_cast<bool>(bit_ref); // 此时会访问 *seg & mask,但 seg 指针是 std::vector<bool> 临时对象的指针,在上一个语句完成后被析构了,所以这里是悬挂指针,未定义行为
// 所以说正确形式应该为
auto bit__ = static_cast<bool>(features(w)[5]);
- 有时候用会使用显式的类型说明,例如
float a = 1.;
来隐式地表示“我要降低精度”,这种情况用auto a = static_cast<float>
代替,可读性会更高,可理解性也更高