ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

auto 类型推导到底推成了什么:与 decltype 的差异

auto 类型推导到底推成了什么:与 decltype 的差异 auto能少写类型、让代码更干净但它绝不是「把等号右边的类型原样抄过来」它会悄悄丢掉顶层 consttop-level const和引用。而decltype走另一条路它要的是「这个表达式在编译器眼里到底是什么类型」。这篇用static_assertstd::is_same把两者到底推成 / 算出什么一条条实证出来避免凭感觉记。官方文档auto 类型说明符、decltype 说明符、std::is_same1. 引子为什么auto x ci;之后x还能被改先抛一个反直觉的现象const int ci 10; auto x ci; // ci 是 const int但 x 不是 const x 20; // 居然能编译通过如果你以为auto会原样保留const这里就踩坑了。下面用static_assert把推导结果「钉死」比任何口头描述都可信。2. auto 会丢掉什么顶层 const 与引用auto按值推导时会剥掉顶层 const并忽略引用它看的是「被引用对象的类型」。// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo #include iostream #include type_traits int main() { const int ci 10; // 顶层 consttop-level const int i 0; int ir i; // ir 是引用 auto a ci; // 顶层 const 被剥 int auto r ir; // 引用被忽略 int static_assert(std::is_samedecltype(a), int::value, a 应是 int); static_assert(std::is_samedecltype(r), int::value, r 应是 int); std::cout auto 从 const int 推出: int std::is_samedecltype(a), int::value \n; std::cout auto 从 int 推出: int std::is_samedecltype(r), int::value \n; }auto 从 const int 推出: int 1 auto 从 int 推出: int 1打印的1表示std::is_same...::value为true。结论auto按值推导 干净的「对象类型」不带 const、不带引用。3. 用 auto / const auto 把引用和 const 留住想要引用或保留 const就显式写auto/const auto。const auto最稳既能绑普通对象也能绑const对象且绝不意外拷贝。#include iostream #include type_traits int main() { const int ci 10; int i 0; auto r1 i; // int const auto r2 ci; // const int可绑 const也可绑非 const static_assert(std::is_samedecltype(r1), int::value, ); static_assert(std::is_samedecltype(r2), const int::value, ); r1 5; // 通过引用改原值 std::cout i 被 r1 改成: i \n; std::cout r1 是 int std::is_samedecltype(r1), int::value \n; std::cout r2 是 const int std::is_samedecltype(r2), const int::value \n; }i 被 r1 改成: 5 r1 是 int 1 r2 是 const int 1把推导规则汇总成一张表括号内是「是否保留」写法从const int推导结果顶层 const引用autoint丢丢autoint保留原对象非 const保留const autoconst int保留保留4. decltype 的两种面孔decltype(x) 与 decltype((x))decltype的怪点在于括号层数会改变结果。decltype(变量名)直接取该变量声明时的类型原样保留 const 和引用。decltype((变量名))里面是一个表达式双层括号让它变成「带括号的变量表达式」是个左值于是取「这个表达式的类型」——对变量名来说就是「它的引用类型」。#include iostream #include type_traits int main() { int x 0; const int cx 0; static_assert(std::is_samedecltype(x), int::value, ); static_assert(std::is_samedecltype(cx), const int::value, ); static_assert(std::is_samedecltype((x)), int::value, ); // 左值表达式 int static_assert(std::is_samedecltype((cx)), const int::value, ); // 左值 const int std::cout decltype(x) : int std::is_samedecltype(x), int::value \n; std::cout decltype(cx) : const int std::is_samedecltype(cx), const int::value \n; std::cout decltype((x)) : int std::is_samedecltype((x)), int::value \n; std::cout decltype((cx)) : const int std::is_samedecltype((cx)), const int::value \n; }decltype(x) : int 1 decltype(cx) : const int 1 decltype((x)) : int 1 decltype((cx)) : const int 1官方文档decltype 的推导规则 里「如果实参是带括号的表达式」这一段正是decltype((x))返回引用的依据。用一张图记住「括号的魔法」decltype( x ) - 看「x 这个名字声明成什么」 - int 原样 decltype((x)) - 看「(x) 这个表达式是什么」 - int 左值表达式 - 引用 ^多一层括号 从「实体」变成「表达式」 类型从值变成引用5. 什么时候必须用 decltype返回类型取决于实参一个典型场景写一个函数它的返回类型要精确等于某个表达式的类型比如返回引用。如果用auto会按值返回、把引用丢掉了调用方拿到的就是拷贝。#include iostream #include type_traits #include vector // 返回类型取决于实参auto 会按值返回丢掉引用decltype(auto) 精确保留 template typename Container, typename Index decltype(auto) access(Container c, Index i) { return std::forwardContainer(c)[i]; // std::vector::operator[] 返回 int } int main() { std::vectorint v{1, 2, 3}; decltype(auto) x access(v, 0); // x 推导为 int不是 int 拷贝 static_assert(std::is_samedecltype(x), int::value, ); x 99; // 改动通过引用反映到 v std::cout 经 x 改后 v[0] v[0] \n; std::cout x 是 int std::is_samedecltype(x), int::value \n; }经 x 改后 v[0] 99 x 是 int 1decltype(auto)是 C14 起的特性C17 默认可用它让返回类型「照抄」初始化表达式的decltype结果。凡是「返回值必须和某表达式类型完全一致含引用」时就用它普通按值返回用auto即可。6. 用 sizeof / typeid 辅助看清推导结果最容易被auto坑的还有数组退化array decay数组名赋给auto会变成指针sizeof直接暴露差异。#include iostream #include type_traits int main() { int arr[5] {1, 2, 3, 4, 5}; auto a arr; // 数组退化成指针 int* static_assert(std::is_samedecltype(a), int*::value, ); std::cout sizeof(arr) sizeof(arr) (5 个 int 20 字节)\n; std::cout sizeof(a) sizeof(a) (指针大小 8 字节)\n; std::cout auto 从数组推出: int* std::is_samedecltype(a), int*::value \n; }sizeof(arr) 20 (5 个 int 20 字节) sizeof(a) 8 (指针大小 8 字节) auto 从数组推出: int* 1sizeof(arr)仍是 20数组本体sizeof(a)变成 864 位平台指针大小。一句话证明a已经不是数组了。想保留数组类型用auto或std::array/std::span。7. 完整示例按值 / 按引用 / decltype(auto) 三选一的后果把前面的点串起来同一个容器元素三种接收方式带来三种行为。#include iostream #include type_traits #include string #include vector class Store { std::vectorstd::string data_; public: explicit Store(std::size_t n) : data_(n, item) {} std::string at(std::size_t i) { return data_.at(i); } const std::string at(std::size_t i) const { return data_.at(i); } }; int main() { Store s{2}; auto copy s.at(0); // 按值拿到拷贝 copy changed-copy; std::cout auto 按值后容器内: s.at(0) \n; auto ref s.at(0); // 按引用改它直接改原容器 ref changed-ref; std::cout auto 后容器内: s.at(0) \n; decltype(auto) exact s.at(1); // 精确保留返回类型 string exact changed-decltype; std::cout decltype(auto) 后容器内: s.at(1) \n; }auto 按值后容器内: item auto 后容器内: changed-ref decltype(auto) 后容器内: changed-decltype8. 性能与内存视角auto 按值就是一次拷贝前面几节都在盯「类型对不对」但auto的代价同样值得盯auto x ...是按值拿到的是副本。元素便宜时无所谓一旦元素是std::string、std::vector一次遍历就可能白白拷出成千上万个字节。最典型的是 range-for// 编译: g -stdc17 -Wall -O2 demo.cpp -o demo #include iostream #include string #include vector int main() { std::vectorstd::string v{alpha, beta, gamma}; for (auto s : v) { // 每个元素拷一份改的只是副本 s (char)33; // 33 就是字符 ! } std::cout auto 后 v[0] v[0] \n; for (auto s : v) { // 引用原元素零拷贝 s (char)33; } std::cout auto 后 v[0] v[0] \n; for (const auto s : v) { // 只读遍历的首选零拷贝 改不动 std::cout 只读遍历 s \n; } }auto 后 v[0] alpha auto 后 v[0] alpha! 只读遍历 alpha! 只读遍历 beta! 只读遍历 gamma!auto s那一轮把alpha拷成了一个临时std::string堆上多一次分配改完就丢所以v[0]纹丝不动auto才真正改到容器里的对象。结论遍历的默认选法是const auto只读或auto要改只有元素本身很便宜int、指针、char时才写auto图省事。泛型代码里更稳的写法是auto它推导成转发引用既不丢 const 也不产生拷贝左值和右值都能绑。再说一点内存const auto r expr;绑定临时对象时会把临时对象的生命周期延长到r的作用域结束所以const auto name makeName();是安全的但如果makeName()返回的本来就是引用r能不能用就完全取决于那个被引用对象还活着——auto在这里帮不了你。9. 易错点三个最容易踩的坑坑一auto从花括号列表推出的是std::initializer_list不是你想要的那个类型。#include initializer_list #include type_traits auto a {1, 2, 3}; // std::initializer_listint不是 int[3]也不是 vector auto b {7}; // 只有一个元素依然推出 initializer_listint static_assert(std::is_samedecltype(a), std::initializer_listint::value, ); static_assert(std::is_samedecltype(b), std::initializer_listint::value, );想要单个值就写int b {7};想要定长数组就写std::array。坑二decltype(auto)会把「表达式」的引用性一并抄回来包括悬垂引用。// 反例不要这么写返回局部变量的引用调用方拿到的是悬垂引用UB decltype(auto) broken() { int local 42; return (local); // (local) 是左值 推导成 int函数一返回 local 就销毁了 }去掉括号写成return local;才是按值返回、类型为int。decltype(auto)只适合「返回的东西确实活得比函数久」的场景比如返回成员引用或容器元素引用。坑三auto声明多个变量时类型必须推成同一个。auto i 1, d 2.5; // 反例不要这么写i 推出 int、d 推出 double编译失败auto不是「每个变量各自的占位符」一整条声明共享同一个推导结果。同理auto f() { return ci; }这种「返回类型写auto」也是按值返回const 会被剥掉、引用会退化——要保留就用decltype(auto)。10. 延伸阅读auto — cppreference推导规则完整清单含结构化绑定、decltype(auto)的说明decltype — cppreference括号表达式返回引用的官方依据std::is_same — cppreference这篇用来「实证」推导结果的工具C Core Guidelines类型推导相关条目搜索auto、decltype看什么时候该显式写明类型本知识库内的相关篇目《const 该放哪从顶层 const 到 const 成员函数》 —— const 放在 * 左边还是右边天差地别。《nullptr 取代 NULL 和 0空指针的三种写法和唯一正确解》 —— NULL 在多数实现里就是 0《constexpr / constinit / consteval编译期常量三兄弟怎么选》 —— constexpr 保证编译期常量11. 一句话总结auto按值推导会剥掉顶层 const 和引用想保留就写auto/const autodecltype(名)原样取声明类型、decltype((名))取左值表达式类型带引用凡「返回值类型必须和某表达式完全一致」时用decltype(auto)别再凭感觉记用static_assertstd::is_same钉死最稳。
返回列表