https://cntransgroup.github.io/EffectiveModernCppChinese/1.DeducingTypes/item2.html
https://cntransgroup.github.io/EffectiveModernCppChinese/1.DeducingTypes/item3.html
https://cntransgroup.github.io/EffectiveModernCppChinese/2.Auto/item5.html
https://cntransgroup.github.io/EffectiveModernCppChinese/2.Auto/item6.html
auto
类型推导
auto
与模板类型推导
在 C++ 中,auto
类型推导通常和模板类型推导相同,也分为:
auto x;
auto& x;
const auto& x;
auto&& x;
const auto&& x;
但是只有一个例外:auto
类型推导假定花括号初始化代表 std::initializer_list
,而模板类型推导不这样做
在模板类型推导中,如果出现下面的代码:
auto x = { 11, 23, 9 }; // x 的类型是 std::initializer_list<int>
template<typename T> // 带有与 x 的声明等价的
void f(T param); // 形参声明的模板
f({ 11, 23, 9 }); // 错误!不能推导出 T
f<std::initializer_list<int>>({ 11, 23, 9 }); // 正确,直接指定 T = std::initializer_list<int>
上面的代码会直接报错,因为花括号不能推导出 std::initializer_list
而在 auto
时则不同,x
可以通过 auto
推导出 std::initializer_list<int>
类型
此外,C++ 14 新增的 lambda 表达式的 auto
语法实际上是模板类型参数的语法糖,使用模板类型推导那一套方案
使用 auto
进行返回值类型推导
C++ 支持后置返回类型,如:
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) -> decltype(c[i]) {
authenticateUser();
return c[i];
}
此时,返回值的类型与 c[i]
的类型(有时 operator[]
的返回值并不是引用,如 std::vector
可能返回一个 wrapper 新对象)完全相同
但是如果去掉 decltype
仅使用 auto
进行自动返回值类型推导(C++ 14),如:
template<typename Container, typename Index>
auto authAndAccess(Container&& c, Index i) {
authenticateUser();
return std::forward<Container>(c)[i];
}
std::vector<int> vec{1, 2, 3};
authAndAccess(vec, 1) = 3; // 无法通过编译,它会返回右值而不是左值引用
此时使用 auto
进行推导,它会进行退化!
优先考虑 auto
而不是显式类型推导
可以避免没有初始化的情况,因为 auto
必须显式初始化
可以使用 auto
来让编译器推导闭包的类型(即 lambda 表达式,它作为一个匿名对象,它的类型只有编译器知道)
可以减少一些移植性的问题,例如:
std::vector<>::size()
返回一个std::vector<>::size_type
类型,它与平台和编译器有关,32 位机器上是 4 字节,而 64 位机器上是 8 字节。如果我们直接用unsigned
的话会有问题,直接使用那一串类型别名的话也不好写,可以直接用auto
很少有人注意到
std::unordered_map<K, V>
中的pair
类型为std::pair<const K, V>
,如果写出下面的代码:std::unordered_map<std::string, int> m; for(const std::pair<std::string, int>& p : m) { // 1 } std::pair<const std::string, int> p; const std::pair<const std::string, int>& rp = p; // 2
那么它实际上在 1 和 2 处,会进行一个隐式类型转换,将
std::pair<const K, V>
先拷贝一份,变成std::pair<K, V>
(纯右值),然后由于const
引用可以引用纯右值,所以直接实质化了,这样我们发现修改p.second
并不会修改m
里面的值。使用auto
就不会出现这种错误:std::unordered_map<std::string, int> m; for(const auto& p : m) { } std::pair<const std::string, int> p; const auto& rp = p;
有时 auto
并不会推导出我们期望的类型
std::vector<bool>
是 std::vector<>
的一个特化,STL 特地为我们准备了这个坑。它将内部的 bool
值按照位进行存储,而不是每一个 bool
占据一个字节
我们看看这个 std::vector<bool>
内部是怎么实现 operator[]
的(简化版):
struct Bit_reference {
using Bit_type = unsigned long;
Bit_type* p;
Bit_type mask;
Bit_reference& operator=(bool x) noexcept {
if (x) *p |= mask;
else *p &= ~mask;
return *this;
}
};
template<typename Alloc>
class vector<bool, Alloc> : protected Bvector_base<Alloc> {
...
using reference = Bit_reference;
using size_type = std::size_t;
using iterator = Bit_iterator;
...
[[nodiscard]] constexpr iterator begin() noexcept {
// this->start.p 是第一个字节的 iterator,第二个参数表示是第几个比特位
return iterator(this->start.p, 0);
}
[[nodiscard]] constexpr reference operator[](size_type n) {
return begin()[n];
}
};
可以看到,它返回的并不是 bool&
,而是 Bit_reference&
,这被叫做代理对象
如果我们写出下面的代码:
std::vector<bool> features(const Widget& w);
Widget w;
auto highPriority = features(w)[5];
processWidget(w, highPriority);
上面的代码中,auto
被推导为 Bit_reference
features()
返回一个纯右值,我们调用了 operator[]
,此时实质化了 std::vector<bool>
,但是之后由于 auto
进行了退化,它拷贝了一份,生命周期就结束了(因为已经没有引用绑定它了),它可能会被析构,这是个未定义行为
此时,Bit_reference
中的指针就悬空了!
同样的,在一些数学库中,可能会使用代理对象来延迟计算,例如:
Matrix sum = m1 + m2 + m3 + m4;
这时也可能会出现问题
我们可以使用强制类型转换来保证得到我们想要的结果,如:
auto highPriority = static_cast<bool>(features(w)[5]);
decltype
与 decltype(auto)
decltype
不进行退化,它的作用是进行类型推导。它可以与 auto
混用,即 decltype(auto)
,进行不退化的类型推导
上面的例子可以改成:
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i) {
authenticateUser();
return std::forward<Container>(c)[i];
}
std::vector<int> vec{1, 2, 3};
authAndAccess(vec, 1) = 3; // 正确
decltype(x)
与 decltype((x))
的区别
x
表示一个变量,而 (x)
则表示一个表达式
在 C++ 11 中规定表达式 (x)
是一个左值,因此这两个 decltype
可能一个是右值一个是左值,如:
int x = 0;
decltype(x) y = x; // int
decltype((x)) z = x; // int&
decltype(auto) f() {
int x = 0;
return (x); // decltype((x)) 是 int&,所以 f 返回 int&
return x; // decltype(x) 是 int,所以 f 返回 int
}