问:auto、auto&、auto&& 的作用和区别是什么?
请判断下面变量的类型,并说明它们能否修改原对象:
int value = 10;
const int const_value = 20;
auto a = const_value;
auto& b = const_value;
auto&& c = value;
auto&& d = 42;
答
先记住结论:
| 写法 | 核心语义 | 能绑定什么 | 是否通常产生新对象 |
|---|---|---|---|
auto x = expr |
按值推导 | 左值、右值 | 是 |
auto& x = expr |
左值引用推导 | 左值 | 否 |
auto&& x = expr |
转发引用推导 | 左值、右值 | 否 |
对于开头的代码:
// a 的类型是 int:复制 const_value,顶层 const 被忽略。
auto a = const_value;
// b 的类型是 const int&:引用原对象,并保留 const。
auto& b = const_value;
// c 的声明类型最终是 int&:左值触发引用折叠。
auto&& c = value;
// d 的声明类型是 int&&:右值绑定到右值引用。
auto&& d = 42;
因此:
- 修改
a不影响const_value。 b引用const_value,但不能通过b修改它。- 修改
c就是在修改value。 d绑定临时对象;在d的作用域内,该临时对象的生命周期被延长。
一、auto:按值推导
auto 的规则近似于函数模板的按值参数:
template <class T>
void function(T parameter);
用 auto 声明变量时,可以把 auto 想象成这里的 T。按值推导通常会:
- 去掉引用。
- 去掉顶层
const和顶层volatile。 - 让数组和函数发生按值传参时的退化。
1. 去掉引用和顶层 const
int value = 10;
int& reference = value;
const int const_value = 20;
auto a = reference; // int
auto b = const_value; // int
a、b 都是独立的 int 对象:
a = 100; // 不影响 value
b = 200; // 合法,不影响 const_value
注意,被指向对象的底层 const 不会被去掉:
const int value = 10;
const int* pointer = &value;
auto copied_pointer = pointer; // const int*
这里 pointer 自身可以被复制,但它指向的是 const int,所以 copied_pointer 仍然不能用来修改 value。
2. 数组会退化成指针
int numbers[3] = {1, 2, 3};
auto first = numbers; // int*
auto& second = numbers; // int (&)[3]
first 只保存首元素地址,数组长度信息已经丢失;second 是整个数组的引用,仍然保留长度 3。
3. auto 不等于“什么都不做”
std::string name = "C++";
auto copy = name;
copy 是新的 std::string。auto 只是让编译器推导类型,不会自动把值语义改成引用语义,也不会自动避免复制。
二、auto&:只绑定左值,并保留 cv 限定
auto& 表示推导一个左值引用:
int value = 10;
const int const_value = 20;
auto& first = value; // int&
auto& second = const_value; // const int&
修改 first 会修改原对象:
first = 99;
std::cout << value; // 99
但 second 保留了原对象的 const,不能通过它写入:
// second = 99; // 编译错误
普通 auto& 不能绑定纯右值:
// auto& reference = 42; // 编译错误
如果只想只读访问,同时兼容左值和临时对象,常用 const auto&:
const auto& first = value; // const int&
const auto& second = 42; // const int&,临时对象生命周期延长
const auto& third = makeObject(); // 避免一次不必要的复制
const auto& 是“只读借用”的常见写法;auto& 是“可写借用,但必须尊重原对象的 const 属性”。
三、auto&&:左值变左值引用,右值变右值引用
当 auto&& 中的 auto 需要根据初始化表达式推导时,它是转发引用,也常被称为万能引用。
int value = 10;
const int const_value = 20;
auto&& a = value; // int&
auto&& b = const_value; // const int&
auto&& c = 42; // int&&
auto&& d = std::move(value); // int&&
关键规则是:
- 初始化表达式是左值,
auto&&最终折叠成左值引用。 - 初始化表达式是右值,
auto&&最终是右值引用。
引用折叠规则
引用折叠一共四种:
T& & -> T&
T& && -> T&
T&& & -> T&
T&& && -> T&&
可以简化成一句话:只要组合中出现左值引用 &,结果就是左值引用;只有两个 && 相遇,结果才是右值引用。
例如:
int value = 10;
auto&& reference = value;
因为 value 是左值,auto 被推导为 int&。代回声明后相当于:
int& && reference = value;
经过引用折叠,最终类型是 int&。
四、声明为 T&& 的变量,表达式仍然可能是左值
这是最容易答错的面试点:
auto&& value = 42; // value 的声明类型是 int&&
虽然 decltype(value) 是 int&&,但是表达式 value 有名字,所以使用 value 时它是左值:
static_assert(std::is_same_v<decltype(value), int&&>);
static_assert(std::is_same_v<decltype((value)), int&>);
如果要恢复初始化时的值类别,需要显式转换:
consume(std::move(value));
在泛型代码里,应该使用 std::forward:
auto wrapper = [](auto&& argument) {
consume(std::forward<decltype(argument)>(argument));
};
- 调用
wrapper(lvalue)时,继续把参数作为左值传递。 - 调用
wrapper(Temporary{})时,继续把参数作为右值传递。
这就是完美转发的基础。
五、auto&& 不总是“右值引用”
&& 是否为转发引用,要看是否发生了特殊的类型推导:
auto&& first = expression; // 是转发引用
auto lambda = [](auto&& value) { // 是转发引用
};
const auto&& second = expression; // 不是转发引用,带 const 后只接右值
同样,在普通函数中已经写死的类型不是转发引用:
void function(std::string&& value); // 只接 std::string 右值
只有类似 T&& 或 auto&&,并且 T / auto 正在按对应规则推导时,才可能根据实参在 & 和 && 之间变化。
六、范围 for 应该选哪个?
假设有:
std::vector<std::string> names = {"C", "C++", "Rust"};
1. auto:逐个复制
for (auto name : names) {
name += "!"; // 修改副本,不影响 names
}
适合确实需要副本,或者元素很小、复制成本可忽略的场景。
2. auto&:直接修改元素
for (auto& name : names) {
name += "!"; // 修改 names 中的元素
}
3. const auto&:只读且避免复制
for (const auto& name : names) {
std::cout << name << '\n';
}
这是遍历大型对象并只读访问时最常见的选择。
4. auto&&:最通用的元素绑定
for (auto&& name : names) {
// 对普通 vector<string>,这里的 name 是 string&。
}
auto&& 可以同时适配返回真正引用或代理对象的迭代器,例如某些视图和 std::vector<bool>。编写泛型范围算法时它更通用;在普通业务代码里,如果明确只处理左值容器,auto& 或 const auto& 往往更直观。
七、花括号初始化的特殊规则
auto 遇到花括号时还有一组常考规则:
auto a = {1, 2, 3}; // std::initializer_list<int>
auto b{1}; // int
// auto c{1, 2}; // 编译错误:直接列表初始化只能有一个元素
// auto d = {1, 2.0}; // 编译错误:元素无法推导成同一种类型
所以不要仅凭右侧“看起来像整数”就断定 auto 一定是 int。
八、auto 与 decltype(auto) 的区别
auto 使用模板式推导,经常去掉引用和顶层 const;decltype(auto) 则严格遵循 decltype 规则,能够保留表达式的精确类型和值类别。
const int value = 10;
auto a = (value); // int
decltype(auto) b = (value); // const int&
函数需要原样返回某个表达式时,decltype(auto) 很有用:
decltype(auto) getElement(Container& container, std::size_t index)
{
return container[index];
}
但它对括号非常敏感:
int value = 10;
decltype(auto) a = value; // int
decltype(auto) b = (value); // int&
九、完整可运行示例
#include <iostream>
#include <type_traits>
#include <utility>
int main()
{
int value = 10;
const int const_value = 20;
auto copied = const_value;
auto& const_reference = const_value;
auto&& lvalue_reference = value;
auto&& rvalue_reference = 42;
static_assert(std::is_same_v<decltype(copied), int>);
static_assert(
std::is_same_v<decltype(const_reference), const int&>);
static_assert(
std::is_same_v<decltype(lvalue_reference), int&>);
static_assert(
std::is_same_v<decltype(rvalue_reference), int&&>);
// 有名字的变量表达式是左值,即使声明类型为 int&&。
static_assert(
std::is_same_v<decltype((rvalue_reference)), int&>);
copied = 100;
lvalue_reference = 30;
rvalue_reference = 50;
int numbers[3] = {1, 2, 3};
auto pointer = numbers;
auto& array_reference = numbers;
static_assert(std::is_same_v<decltype(pointer), int*>);
static_assert(
std::is_same_v<decltype(array_reference), int (&)[3]>);
std::cout << "copied: " << copied << '\n'
<< "const_value: " << const_value << '\n'
<< "value: " << value << '\n'
<< "temporary through reference: "
<< rvalue_reference << '\n'
<< "array size: "
<< std::extent_v<std::remove_reference_t<
decltype(array_reference)>>
<< '\n';
}
编译运行:
g++ -std=c++17 -Wall -Wextra -pedantic auto_reference.cpp \
-o auto_reference
./auto_reference
输出:
copied: 100
const_value: 20
value: 30
temporary through reference: 50
array size: 3
十、常见错误
错误 1:认为 auto 会保留引用
int value = 10;
int& reference = value;
auto copy = reference; // int,不是 int&
需要引用时必须显式写 auto& 或根据场景写 auto&&。
错误 2:认为 auto&& 永远是右值引用
int value = 10;
auto&& reference = value; // int&
初始化表达式是左值时,结果是左值引用。
错误 3:把有名字的右值引用变量继续当成右值
auto&& object = makeObject();
consume(object); // object 表达式是左值
consume(std::move(object)); // 显式转成右值
泛型代码不要无条件 std::move,应该用 std::forward 保留调用者传入的值类别。
错误 4:为了避免复制,到处写 auto&&
auto&& 很通用,但也更容易隐藏对象生命周期和值类别。普通代码应按意图选择:
- 需要独立副本:
auto。 - 需要修改已有左值:
auto&。 - 只读且避免复制:
const auto&。 - 需要兼容左值、右值并保留值类别:
auto&&配合std::forward。
面试总结
面试时可以这样回答:
auto是按值推导,会去掉引用和顶层 const,并可能让数组、函数退化;变量通常是一个新对象。auto&推导左值引用,只能绑定左值,保留原对象的 const 属性,修改它会修改原对象。auto&&在发生 auto 类型推导时是转发引用:绑定左值会通过引用折叠得到左值引用,绑定右值才得到右值引用。还要注意,即使变量的声明类型是右值引用,只要变量有名字,这个变量表达式就是左值;泛型转发时要使用 std::forward。
最后记住四句话:
auto:我要一个值。auto&:我要借用并可能修改一个左值。const auto&:我要只读借用,兼容临时对象。auto&&:我要根据初始化表达式保留左值或右值属性。