From 7256ed40590b723e40429cd6e77a0394f5b55b9e Mon Sep 17 00:00:00 2001 From: archibate <1931127624@qq.com> Date: Sat, 9 Nov 2024 12:46:05 +0800 Subject: [PATCH] almost complete auto --- docs/auto.md | 291 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 287 insertions(+), 4 deletions(-) diff --git a/docs/auto.md b/docs/auto.md index 25af12c..e246132 100644 --- a/docs/auto.md +++ b/docs/auto.md @@ -1,6 +1,12 @@ -# `auto` 神教 (未完工) +# `auto` 神教 -## 变量 `auto` +## `auto` 关键字的前世今生 + +TODO + +## 变量声明为 `auto` + +TODO ## 返回类型 `auto` @@ -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 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 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 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`,所以即使你只需修改 `V` 的部分,只需使用 `auto &` 配合 C++17 的“结构化绑定” (structural-binding) 语法拆包即可,`K` 的部分会自动带上 `const`,不会出现编译错误的。 + +```cpp +std::map table; +for (auto &[k, v]: table) { // 编译通过:k 的部分会自动带上 const + k = "hello"; // 编译出错:k 推导为 std::string const & 不可修改 + v = "world"; // 没问题:v 推导为 std::string & 可以就地修改 +} +``` + ## 参数类型 `auto` C++20 引入了**模板参数推导**,可以让我们在函数参数中也使用 `auto`。 @@ -142,6 +335,96 @@ int main() { } ``` -## `auto` 推导为引用 +实际上等价于模板函数的如下写法: + +```cpp +template +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` 帮手函数介绍