Skip to content

Commit

Permalink
almost complete auto
Browse files Browse the repository at this point in the history
  • Loading branch information
archibate committed Nov 9, 2024
1 parent 4c4ad97 commit 7256ed4
Showing 1 changed file with 287 additions and 4 deletions.
291 changes: 287 additions & 4 deletions docs/auto.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# `auto` 神教 (未完工)
# `auto` 神教

## 变量 `auto`
## `auto` 关键字的前世今生

TODO

## 变量声明为 `auto`

TODO

## 返回类型 `auto`

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

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

### 返回引用类型

返回类型声明为 `auto`,可以自动推导返回类型,但总是推导出普通的值类型,绝对不会带有引用或 `const` 修饰。

如果需要返回一个引用,并且希望自动推导引用的类型,可以写 `auto &`

```cpp
int i;
int &ref = i;

auto f() { // 返回类型推导为 int
return i;
}

auto f() { // 返回类型推导依然为 int
return ref;
}

auto &f() { // 返回类型这才能推导为 int &
return ref;
}

auto &f() { // 编译期报错:1 是纯右值,不可转为左值引用
return 1;
}

auto &f() { // 运行时出错:空悬引用是未定义行为
int local = 42;
return local;
}
```

这里的 `auto` 还可以带有 `const` 修饰,例如 `auto const &` 可以让返回类型变成带有 `const` 修饰的常引用。

```cpp
int i;
int &ref = i;

```cpp
int i;

auto getValue() { // 返回类型推导为 int
return i;
}

auto &getRef() { // 返回类型推导为 int &
return i;
}

auto const &getConstRef() { // 返回类型推导为 int const &
return i;
}
```

> {{ icon.tip }} `auto const &``const auto &` 完全等价,只是代码习惯问题。
有趣的是,如果 `i``int const` 类型,则 `auto &` 也可以自动推导为 `int const &` 且不报错。

```cpp
const int i;

auto const &getConstRef() { // 返回类型推导为 int const &
return i;
}

auto &getRef() { // 返回类型也会被推导为 int const &
return i;
}

int &getRef() { // 报错!
return i;
}
```

> {{ icon.tip }} `int const``const int` 是完全等价的,只是代码习惯问题。
> {{ icon.detail }} `auto &` 可以兼容 `int const &`,而 `int &` 就不能兼容 `int const &`!很奇怪吧?这是因为 `auto` 不一定必须是 `int`,也可以是 `const int` 这一整个类型。你可以把 `auto` 看作和模板函数参数一样,模板函数参数的 `T &` 一样可以通过将 `T = const int` 从而捕获 `const int &`
如果要允许 `auto` 推导为右值引用,只需写 `auto &&`

```cpp
std::string str;

auto &&getRVRef() { // std::string &&
return std::move(str);
}

auto &getRef() { // std::string &
return str;
}

auto const &getConstRef() { // std::string const &
return str;
}
```

正如 `auto &` 可以兼容 `auto const &` 一样,由于 C++ 的某些特色机制,`auto &&` 其实也可以兼容 `auto &`

所以 `auto &&` 实际上不止支持右值引用,也支持左值引用,因此被称为“万能引用”。

也就是说,其实我们可以都写作 `auto &&`!让编译器自动根据我们 `return` 语句的表达式类型,判断返回类型是左还是右引用。

```cpp
std::string str;

auto &&getRVRef() { // std::string &&
return std::move(str);
}

auto &&getRef() { // std::string &
return str;
}

auto const &getConstRef() { // std::string const &
return str;
}
```

`auto &&` 不仅能推导为右值引用,也能推导为左值引用,常左值引用。

可以理解为集合的包含关系:`auto &&` > `auto &` > `auto const &`

所以 `auto &&` 实际上可以推导所有引用,不论左右。

> {{ icon.detail }} 这里的原因和刚才 `auto = int const` 从而 `auto &` 可以接纳 `int const &` 一样,`auto &&` 可以接纳 `int &` 是因为 C++ 特色的“引用折叠”机制:`& && = &` 即左引用碰到右引用,会得到左引用。所以编译器可以通过令 `auto = int &` 从而使得 `auto && = int & && = int &`,从而实际上 `auto &&` 看似是右值引用,但是因为可以给 `auto` 带入一个左值引用 `int &`,然后让左引用 `&` 与右引用 `&&` “湮灭”,最终只剩下一个左引用 `&`,在之后的模板函数专题中会更详细介绍这一特色机制。
这就是为什么 `int &&` 就只是右值引用,而 `auto &&` 以及 `T &&` 则会叫做万能引用。一旦允许前面的参数为 `auto` 或者模板参数,就可以代换,就可以实现左右通吃。

### 真正的万能 `decltype(auto)`

以上介绍的这些引用推导规则,其实也适用于局部变量的 `auto`,例如:

```cpp
auto i = 0; // int i = 0
auto &ref = i; // int &ref = i
auto const &cref = i; // int const &cref = i
auto &&rvref = move(i); // int &&rvref = move(i)

decltype(auto) j = i; // int j = i
decltype(auto) k = ref; // int &k = ref
decltype(auto) l = cref; // int const &l = cref
decltype(auto) m = move(rvref); // int &&m = rvref
```
## 范围 for 循环中的 `auto &`
众所周知,在 C++11 的“范围 for 循环” (range-based for loop) 语法中,`auto` 的出镜率很高。
但是如果只是写 `auto i: arr` 的话,这会从 arr 中拷贝一份新的 `i` 变量出来,不仅产生了额外的开销,还意味着你对这 `i` 变量的修改不会反映到 `arr` 中原本的元素中去。
```cpp
std::vector<int> arr = {1, 2, 3};
for (auto i: arr) { // auto i 推导为 int i,会拷贝一份新的 int 变量
i += 1; // 错误的写法,这样只是修改了 int 变量
}
print(arr); // 依然是 {1, 2, 3}
```

更好的写法是 `auto &i: arr`,保存一份对数组中元素的引用,不仅避免了拷贝的开销(如果不是 `int` 而是其他更大的类型的话,这是一笔不小的开销),而且允许你就地修改数组中元素的值。

```cpp
std::vector<int> arr = {1, 2, 3};
for (auto &i: arr) { // auto &i 推导为 int &i,保存的是对 arr 中原元素的一份引用,不发生拷贝
i += 1; // 因为 i 现在是对 arr 中真正元素的引用,对其修改也会成功反映到原 arr 中去
}
print(arr); // 变成了 {2, 3, 4}
```
如果不打算修改数组,也可以用 `auto const &`,让捕获到的引用添加上 `const` 修饰,避免一不小心修改了数组,同时提升代码可读性(人家一看就懂哪些 for 循环是想要修改原值,哪些不会修改原值)。
```cpp
std::vector<int> arr = {1, 2, 3};
for (auto const &i: arr) { // auto const &i 推导为 int const &i,保存的是对 arr 中原元素的一份常引用,不发生拷贝,且不可修改
i += 1; // 编译期出错!const 引用不可修改
}
```

> {{ icon.tip }} 对于遍历 `std::map`,由于刚才提到的 `auto &` 实际上也兼容常引用,而 map 的值类型是 `std::pair<const K, V>`,所以即使你只需修改 `V` 的部分,只需使用 `auto &` 配合 C++17 的“结构化绑定” (structural-binding) 语法拆包即可,`K` 的部分会自动带上 `const`,不会出现编译错误的。
```cpp
std::map<std::string, std::string> table;
for (auto &[k, v]: table) { // 编译通过:k 的部分会自动带上 const
k = "hello"; // 编译出错:k 推导为 std::string const & 不可修改
v = "world"; // 没问题:v 推导为 std::string & 可以就地修改
}
```

## 参数类型 `auto`

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

## `auto` 推导为引用
实际上等价于模板函数的如下写法:

```cpp
template <class T>
decltype(T() * T()) square(T x) {
return x * x;
}
```
### 参数 `auto` 推导为引用
和之前变量 `auto`,返回类型 `auto` 的 `auto &`、`auto const &`、`auto &&` 大差不差,C++20 这个参数 `auto` 同样也支持推导为引用。
```cpp
void passByValue(auto x) { // 参数类型推导为 int
x = 42;
}
void passByRef(auto &x) { // 参数类型推导为 int &
x = 42;
}
void passByConstRef(auto const &x) { // 参数类型推导为 int const &
x = 42; // 编译期错误:常引用无法写入!
}
int x = 1;
passByValue(x);
cout << x; // 还是 1
passByRef(x);
cout << x; // 42
```

```cpp
void passByRef(auto &x) {
x = 1;
}

int x = 1;
const int const_x = 1;
passByRef(i); // 参数类型推导为 int &
passByRef(const_x); // 参数类型推导为 const int &
```
由于 `auto &` 兼容 `auto const &` 的尿性,此处第二个调用 `passByRef` 会把参数类型推导为 `const int &`,这会导致里面的 x = 42 编译出错!
- 所以 `auto &` 实际上也允许传入 `const` 变量的引用,非常恼人,不要掉以轻心。
- 而 `auto const &` 则可以安心,一定是带 `const` 的。
> {{ icon.fun }} 所以实际上最常用的是 `auto const &`。
不仅如此 `auto const &` 参数还可以传入纯右值(利用了 C++ 可以自动把纯右值转为 `const` 左引用的特性)。
对于已有的变量传入,可以避免一次拷贝;对于就地创建的纯右值表达式,则自动转换,非常方便。
```cpp
void passByConstRef(auto const &cref) {
std::cout << cref;
}
int i = 42;
passByConstRef(i); // 传入 i 的引用
passByConstRef(42); // 利用 C++ 自动把纯右值 “42” 自动转为 const 左值的特性
```

对于这种自动转出来的 `const` 左值引用,其实际上是在栈上自动创建了一个 `const` 变量保存你临时创建的参数,然后在当前行结束后自动析构。

```cpp
passByConstRef(42);
// 等价于:
{
const int tmp = 42;
passByConstRef(tmp); // 传入的是这个自动生成 tmp 变量的 const 引用
}
```
这个自动生成的 `tmp` 变量的生命周期是“一条语句”,也就是当前分号结束前,该变量的生命周期都存在,直到分号结束后才会析构,所以如下代码是安全的:
```cpp
void someCFunc(const char *name);
someCFunc(std::string("hello").c_str());
```

> {{ icon.detail }} 此处 `std::string("hello")` 构造出的临时 `string` 类型变量的生命周期直到 `;` 才结束,而这时 `someCFunc` 早已执行完毕返回了,只要 `someCFunc``name` 的访问集中在当前这次函数调用中,没有把 `name` 参数存到全局变量中去,就不会有任何空悬指针问题。
### `auto &&` 参数万能引用及其转发

TODO

然而,由于 C++ “默认自动变左值”的糟糕特色,即使你将一个传入时是右值的引用直接转发给另一个函数,这个参数也会默默退化成左值类型,需要再 `std::move` 一次才能保持他一直处于右值类型。

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

0 comments on commit 7256ed4

Please sign in to comment.