|
1 |
| -# `auto` 神教 (未完工) |
| 1 | +# `auto` 神教 |
2 | 2 |
|
3 |
| -## 变量 `auto` |
| 3 | +## `auto` 关键字的前世今生 |
| 4 | + |
| 5 | +TODO |
| 6 | + |
| 7 | +## 变量声明为 `auto` |
| 8 | + |
| 9 | +TODO |
4 | 10 |
|
5 | 11 | ## 返回类型 `auto`
|
6 | 12 |
|
@@ -64,6 +70,193 @@ auto f() { // 编译通过:auto 推导为 int
|
64 | 70 |
|
65 | 71 | 因此,`auto` 通常只适用于头文件中“就地定义”的 `inline` 函数,不适合需要“分离 .cpp 文件”的函数。
|
66 | 72 |
|
| 73 | +### 返回引用类型 |
| 74 | + |
| 75 | +返回类型声明为 `auto`,可以自动推导返回类型,但总是推导出普通的值类型,绝对不会带有引用或 `const` 修饰。 |
| 76 | + |
| 77 | +如果需要返回一个引用,并且希望自动推导引用的类型,可以写 `auto &`。 |
| 78 | + |
| 79 | +```cpp |
| 80 | +int i; |
| 81 | +int &ref = i; |
| 82 | + |
| 83 | +auto f() { // 返回类型推导为 int |
| 84 | + return i; |
| 85 | +} |
| 86 | + |
| 87 | +auto f() { // 返回类型推导依然为 int |
| 88 | + return ref; |
| 89 | +} |
| 90 | + |
| 91 | +auto &f() { // 返回类型这才能推导为 int & |
| 92 | + return ref; |
| 93 | +} |
| 94 | + |
| 95 | +auto &f() { // 编译期报错:1 是纯右值,不可转为左值引用 |
| 96 | + return 1; |
| 97 | +} |
| 98 | + |
| 99 | +auto &f() { // 运行时出错:空悬引用是未定义行为 |
| 100 | + int local = 42; |
| 101 | + return local; |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +这里的 `auto` 还可以带有 `const` 修饰,例如 `auto const &` 可以让返回类型变成带有 `const` 修饰的常引用。 |
| 106 | + |
| 107 | +```cpp |
| 108 | +int i; |
| 109 | +int &ref = i; |
| 110 | + |
| 111 | +```cpp |
| 112 | +int i; |
| 113 | + |
| 114 | +auto getValue() { // 返回类型推导为 int |
| 115 | + return i; |
| 116 | +} |
| 117 | + |
| 118 | +auto &getRef() { // 返回类型推导为 int & |
| 119 | + return i; |
| 120 | +} |
| 121 | + |
| 122 | +auto const &getConstRef() { // 返回类型推导为 int const & |
| 123 | + return i; |
| 124 | +} |
| 125 | +``` |
| 126 | + |
| 127 | +> {{ icon.tip }} `auto const &` 与 `const auto &` 完全等价,只是代码习惯问题。 |
| 128 | +
|
| 129 | +有趣的是,如果 `i` 是 `int const` 类型,则 `auto &` 也可以自动推导为 `int const &` 且不报错。 |
| 130 | + |
| 131 | +```cpp |
| 132 | +const int i; |
| 133 | + |
| 134 | +auto const &getConstRef() { // 返回类型推导为 int const & |
| 135 | + return i; |
| 136 | +} |
| 137 | + |
| 138 | +auto &getRef() { // 返回类型也会被推导为 int const & |
| 139 | + return i; |
| 140 | +} |
| 141 | + |
| 142 | +int &getRef() { // 报错! |
| 143 | + return i; |
| 144 | +} |
| 145 | +``` |
| 146 | + |
| 147 | +> {{ icon.tip }} `int const` 与 `const int` 是完全等价的,只是代码习惯问题。 |
| 148 | +
|
| 149 | +> {{ icon.detail }} `auto &` 可以兼容 `int const &`,而 `int &` 就不能兼容 `int const &`!很奇怪吧?这是因为 `auto` 不一定必须是 `int`,也可以是 `const int` 这一整个类型。你可以把 `auto` 看作和模板函数参数一样,模板函数参数的 `T &` 一样可以通过将 `T = const int` 从而捕获 `const int &`。 |
| 150 | +
|
| 151 | +如果要允许 `auto` 推导为右值引用,只需写 `auto &&`。 |
| 152 | + |
| 153 | +```cpp |
| 154 | +std::string str; |
| 155 | + |
| 156 | +auto &&getRVRef() { // std::string && |
| 157 | + return std::move(str); |
| 158 | +} |
| 159 | + |
| 160 | +auto &getRef() { // std::string & |
| 161 | + return str; |
| 162 | +} |
| 163 | + |
| 164 | +auto const &getConstRef() { // std::string const & |
| 165 | + return str; |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +正如 `auto &` 可以兼容 `auto const &` 一样,由于 C++ 的某些特色机制,`auto &&` 其实也可以兼容 `auto &`! |
| 170 | + |
| 171 | +所以 `auto &&` 实际上不止支持右值引用,也支持左值引用,因此被称为“万能引用”。 |
| 172 | + |
| 173 | +也就是说,其实我们可以都写作 `auto &&`!让编译器自动根据我们 `return` 语句的表达式类型,判断返回类型是左还是右引用。 |
| 174 | + |
| 175 | +```cpp |
| 176 | +std::string str; |
| 177 | + |
| 178 | +auto &&getRVRef() { // std::string && |
| 179 | + return std::move(str); |
| 180 | +} |
| 181 | + |
| 182 | +auto &&getRef() { // std::string & |
| 183 | + return str; |
| 184 | +} |
| 185 | + |
| 186 | +auto const &getConstRef() { // std::string const & |
| 187 | + return str; |
| 188 | +} |
| 189 | +``` |
| 190 | + |
| 191 | +`auto &&` 不仅能推导为右值引用,也能推导为左值引用,常左值引用。 |
| 192 | + |
| 193 | +可以理解为集合的包含关系:`auto &&` > `auto &` > `auto const &` |
| 194 | + |
| 195 | +所以 `auto &&` 实际上可以推导所有引用,不论左右。 |
| 196 | + |
| 197 | +> {{ icon.detail }} 这里的原因和刚才 `auto = int const` 从而 `auto &` 可以接纳 `int const &` 一样,`auto &&` 可以接纳 `int &` 是因为 C++ 特色的“引用折叠”机制:`& && = &` 即左引用碰到右引用,会得到左引用。所以编译器可以通过令 `auto = int &` 从而使得 `auto && = int & && = int &`,从而实际上 `auto &&` 看似是右值引用,但是因为可以给 `auto` 带入一个左值引用 `int &`,然后让左引用 `&` 与右引用 `&&` “湮灭”,最终只剩下一个左引用 `&`,在之后的模板函数专题中会更详细介绍这一特色机制。 |
| 198 | +
|
| 199 | +这就是为什么 `int &&` 就只是右值引用,而 `auto &&` 以及 `T &&` 则会叫做万能引用。一旦允许前面的参数为 `auto` 或者模板参数,就可以代换,就可以实现左右通吃。 |
| 200 | + |
| 201 | +### 真正的万能 `decltype(auto)` |
| 202 | + |
| 203 | +以上介绍的这些引用推导规则,其实也适用于局部变量的 `auto`,例如: |
| 204 | + |
| 205 | +```cpp |
| 206 | +auto i = 0; // int i = 0 |
| 207 | +auto &ref = i; // int &ref = i |
| 208 | +auto const &cref = i; // int const &cref = i |
| 209 | +auto &&rvref = move(i); // int &&rvref = move(i) |
| 210 | + |
| 211 | +decltype(auto) j = i; // int j = i |
| 212 | +decltype(auto) k = ref; // int &k = ref |
| 213 | +decltype(auto) l = cref; // int const &l = cref |
| 214 | +decltype(auto) m = move(rvref); // int &&m = rvref |
| 215 | +``` |
| 216 | +
|
| 217 | +## 范围 for 循环中的 `auto &` |
| 218 | +
|
| 219 | +众所周知,在 C++11 的“范围 for 循环” (range-based for loop) 语法中,`auto` 的出镜率很高。 |
| 220 | +
|
| 221 | +但是如果只是写 `auto i: arr` 的话,这会从 arr 中拷贝一份新的 `i` 变量出来,不仅产生了额外的开销,还意味着你对这 `i` 变量的修改不会反映到 `arr` 中原本的元素中去。 |
| 222 | +
|
| 223 | +```cpp |
| 224 | +std::vector<int> arr = {1, 2, 3}; |
| 225 | +for (auto i: arr) { // auto i 推导为 int i,会拷贝一份新的 int 变量 |
| 226 | + i += 1; // 错误的写法,这样只是修改了 int 变量 |
| 227 | +} |
| 228 | +print(arr); // 依然是 {1, 2, 3} |
| 229 | +``` |
| 230 | + |
| 231 | +更好的写法是 `auto &i: arr`,保存一份对数组中元素的引用,不仅避免了拷贝的开销(如果不是 `int` 而是其他更大的类型的话,这是一笔不小的开销),而且允许你就地修改数组中元素的值。 |
| 232 | + |
| 233 | +```cpp |
| 234 | +std::vector<int> arr = {1, 2, 3}; |
| 235 | +for (auto &i: arr) { // auto &i 推导为 int &i,保存的是对 arr 中原元素的一份引用,不发生拷贝 |
| 236 | + i += 1; // 因为 i 现在是对 arr 中真正元素的引用,对其修改也会成功反映到原 arr 中去 |
| 237 | +} |
| 238 | +print(arr); // 变成了 {2, 3, 4} |
| 239 | +``` |
| 240 | +
|
| 241 | +如果不打算修改数组,也可以用 `auto const &`,让捕获到的引用添加上 `const` 修饰,避免一不小心修改了数组,同时提升代码可读性(人家一看就懂哪些 for 循环是想要修改原值,哪些不会修改原值)。 |
| 242 | +
|
| 243 | +```cpp |
| 244 | +std::vector<int> arr = {1, 2, 3}; |
| 245 | +for (auto const &i: arr) { // auto const &i 推导为 int const &i,保存的是对 arr 中原元素的一份常引用,不发生拷贝,且不可修改 |
| 246 | + i += 1; // 编译期出错!const 引用不可修改 |
| 247 | +} |
| 248 | +``` |
| 249 | + |
| 250 | +> {{ icon.tip }} 对于遍历 `std::map`,由于刚才提到的 `auto &` 实际上也兼容常引用,而 map 的值类型是 `std::pair<const K, V>`,所以即使你只需修改 `V` 的部分,只需使用 `auto &` 配合 C++17 的“结构化绑定” (structural-binding) 语法拆包即可,`K` 的部分会自动带上 `const`,不会出现编译错误的。 |
| 251 | +
|
| 252 | +```cpp |
| 253 | +std::map<std::string, std::string> table; |
| 254 | +for (auto &[k, v]: table) { // 编译通过:k 的部分会自动带上 const |
| 255 | + k = "hello"; // 编译出错:k 推导为 std::string const & 不可修改 |
| 256 | + v = "world"; // 没问题:v 推导为 std::string & 可以就地修改 |
| 257 | +} |
| 258 | +``` |
| 259 | + |
67 | 260 | ## 参数类型 `auto`
|
68 | 261 |
|
69 | 262 | C++20 引入了**模板参数推导**,可以让我们在函数参数中也使用 `auto`。
|
@@ -142,6 +335,96 @@ int main() {
|
142 | 335 | }
|
143 | 336 | ```
|
144 | 337 |
|
145 |
| -## `auto` 推导为引用 |
| 338 | +实际上等价于模板函数的如下写法: |
| 339 | + |
| 340 | +```cpp |
| 341 | +template <class T> |
| 342 | +decltype(T() * T()) square(T x) { |
| 343 | + return x * x; |
| 344 | +} |
| 345 | +``` |
| 346 | +
|
| 347 | +### 参数 `auto` 推导为引用 |
| 348 | +
|
| 349 | +和之前变量 `auto`,返回类型 `auto` 的 `auto &`、`auto const &`、`auto &&` 大差不差,C++20 这个参数 `auto` 同样也支持推导为引用。 |
| 350 | +
|
| 351 | +```cpp |
| 352 | +void passByValue(auto x) { // 参数类型推导为 int |
| 353 | + x = 42; |
| 354 | +} |
| 355 | +
|
| 356 | +void passByRef(auto &x) { // 参数类型推导为 int & |
| 357 | + x = 42; |
| 358 | +} |
| 359 | +
|
| 360 | +void passByConstRef(auto const &x) { // 参数类型推导为 int const & |
| 361 | + x = 42; // 编译期错误:常引用无法写入! |
| 362 | +} |
| 363 | +
|
| 364 | +int x = 1; |
| 365 | +passByValue(x); |
| 366 | +cout << x; // 还是 1 |
| 367 | +passByRef(x); |
| 368 | +cout << x; // 42 |
| 369 | +``` |
| 370 | + |
| 371 | +```cpp |
| 372 | +void passByRef(auto &x) { |
| 373 | + x = 1; |
| 374 | +} |
| 375 | + |
| 376 | +int x = 1; |
| 377 | +const int const_x = 1; |
| 378 | +passByRef(i); // 参数类型推导为 int & |
| 379 | +passByRef(const_x); // 参数类型推导为 const int & |
| 380 | +``` |
| 381 | +
|
| 382 | +由于 `auto &` 兼容 `auto const &` 的尿性,此处第二个调用 `passByRef` 会把参数类型推导为 `const int &`,这会导致里面的 x = 42 编译出错! |
| 383 | +
|
| 384 | +- 所以 `auto &` 实际上也允许传入 `const` 变量的引用,非常恼人,不要掉以轻心。 |
| 385 | +- 而 `auto const &` 则可以安心,一定是带 `const` 的。 |
| 386 | +
|
| 387 | +> {{ icon.fun }} 所以实际上最常用的是 `auto const &`。 |
| 388 | +
|
| 389 | +不仅如此 `auto const &` 参数还可以传入纯右值(利用了 C++ 可以自动把纯右值转为 `const` 左引用的特性)。 |
| 390 | +
|
| 391 | +对于已有的变量传入,可以避免一次拷贝;对于就地创建的纯右值表达式,则自动转换,非常方便。 |
| 392 | +
|
| 393 | +```cpp |
| 394 | +void passByConstRef(auto const &cref) { |
| 395 | + std::cout << cref; |
| 396 | +} |
| 397 | +
|
| 398 | +int i = 42; |
| 399 | +passByConstRef(i); // 传入 i 的引用 |
| 400 | +passByConstRef(42); // 利用 C++ 自动把纯右值 “42” 自动转为 const 左值的特性 |
| 401 | +``` |
| 402 | + |
| 403 | +对于这种自动转出来的 `const` 左值引用,其实际上是在栈上自动创建了一个 `const` 变量保存你临时创建的参数,然后在当前行结束后自动析构。 |
| 404 | + |
| 405 | +```cpp |
| 406 | +passByConstRef(42); |
| 407 | +// 等价于: |
| 408 | +{ |
| 409 | + const int tmp = 42; |
| 410 | + passByConstRef(tmp); // 传入的是这个自动生成 tmp 变量的 const 引用 |
| 411 | +} |
| 412 | +``` |
| 413 | +
|
| 414 | +这个自动生成的 `tmp` 变量的生命周期是“一条语句”,也就是当前分号结束前,该变量的生命周期都存在,直到分号结束后才会析构,所以如下代码是安全的: |
| 415 | +
|
| 416 | +```cpp |
| 417 | +void someCFunc(const char *name); |
| 418 | +
|
| 419 | +someCFunc(std::string("hello").c_str()); |
| 420 | +``` |
| 421 | + |
| 422 | +> {{ icon.detail }} 此处 `std::string("hello")` 构造出的临时 `string` 类型变量的生命周期直到 `;` 才结束,而这时 `someCFunc` 早已执行完毕返回了,只要 `someCFunc` 对 `name` 的访问集中在当前这次函数调用中,没有把 `name` 参数存到全局变量中去,就不会有任何空悬指针问题。 |
| 423 | +
|
| 424 | +### `auto &&` 参数万能引用及其转发 |
| 425 | + |
| 426 | +TODO |
| 427 | + |
| 428 | +然而,由于 C++ “默认自动变左值”的糟糕特色,即使你将一个传入时是右值的引用直接转发给另一个函数,这个参数也会默默退化成左值类型,需要再 `std::move` 一次才能保持他一直处于右值类型。 |
146 | 429 |
|
147 |
| -TODO: 继续介绍 `auto`, `auto const`, `auto &`, `auto const &`, `auto &&`, `decltype(auto)`, `auto *`, `auto const *` |
| 430 | +### `std::forward` 帮手函数介绍 |
0 commit comments