Skip to content

Commit 7256ed4

Browse files
committed
almost complete auto
1 parent 4c4ad97 commit 7256ed4

File tree

1 file changed

+287
-4
lines changed

1 file changed

+287
-4
lines changed

docs/auto.md

+287-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
# `auto` 神教 (未完工)
1+
# `auto` 神教
22

3-
## 变量 `auto`
3+
## `auto` 关键字的前世今生
4+
5+
TODO
6+
7+
## 变量声明为 `auto`
8+
9+
TODO
410

511
## 返回类型 `auto`
612

@@ -64,6 +70,193 @@ auto f() { // 编译通过:auto 推导为 int
6470

6571
因此,`auto` 通常只适用于头文件中“就地定义”的 `inline` 函数,不适合需要“分离 .cpp 文件”的函数。
6672

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+
67260
## 参数类型 `auto`
68261

69262
C++20 引入了**模板参数推导**,可以让我们在函数参数中也使用 `auto`
@@ -142,6 +335,96 @@ int main() {
142335
}
143336
```
144337

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` 一次才能保持他一直处于右值类型。
146429

147-
TODO: 继续介绍 `auto`, `auto const`, `auto &`, `auto const &`, `auto &&`, `decltype(auto)`, `auto *`, `auto const *`
430+
### `std::forward` 帮手函数介绍

0 commit comments

Comments
 (0)