auto 这东西。
我第一次认真看它。
不是在 C++11。
是在更早的老代码里。
那种你一打开就能闻到“2003 年味道”的工程。
你会看到一堆熟悉的老朋友。
static。
extern。
register。
还有一个。
auto。
但那时的 auto。
不是今天这个。
它只是“存储期说明”。
翻译成人话。
就是。
“这就是个普通局部变量”。
问题是。
你不写。
它也默认就是。
所以 auto 在 C++98 里,几乎等于空气。
你真想写也能写。
像这样。
void f() {
auto int i = 0;
}
这段代码合法。
但写出来有点像对编译器说。
“别紧张,我知道这是局部变量。”
编译器一般也不回你。
它只会默默把你当成一个刚学会关键字的人。
后来时代变了。
模板火了。
STL 火了。
你开始写迭代器。
写 traits。
写一堆“类型的类型”。
然后你突然发现。
类型系统这玩意。
确实很强。
强到你要先把名字写完。
才能开始干活。
我见过最典型的一行。
长这样。
std::vector<std::string>::const_iterator it = v.begin();
这行当然没错。
而且很“正统”。
但读它的感觉。
跟在黑板上写一整行化学式差不多。
你知道你在干嘛。
但你不想每天都干。
那几年大家也不是不想偷懒。
大家是没法偷。
GCC 早早给了 typeof。
能用。
也很爽。
比如这样。
typeof(v.begin()) it = v.begin();
但你一转到别的编译器。
尤其是当年那位“脾气很硬”的 MSVC。
项目就开始咳嗽。
Boost 也试过。
BOOST_TYPEOF 那套东西。
很聪明。
也很费劲。
你会觉得自己不是在写业务。
是在写一套给编译器看的外交辞令。
所以到了 C++0x(后来改名 C++11)那段时间。
委员会里有个共识。
类型系统是 C++ 的强项。
但如果强到“写不动”。
那就不是强项了。
那是负担。
于是 auto 被翻新。
把它从“没人用的存储期说明符”。
换成“让编译器替你把类型写出来”。
你可以把它看成一句很朴素的承诺。
auto省掉的是重复。不是责任。
“右边表达式的类型是什么,左边就是什么。”
听起来简单。
但真正改变的是。
你写代码的手感。
第一个场景:迭代器,终于能像人一样写
先看旧写法。
std::vector<int> v{1, 2, 3};
std::vector<int>::iterator it = v.begin();
这段最烦人的地方。
不是复杂。
是重复。
你把容器类型写了两遍。
而你真正关心的只有一件事。
“从 begin() 拿一个迭代器”。
换成 auto。
std::vector<int> v{1, 2, 3};
auto it = v.begin();
少写的不是字符。
是心智负担。
再往前一步。
循环也顺。
for (auto it = v.begin(); it != v.end(); ++it) {
// ...
}
你会发现。
这才像“写程序”。
不是“抄类型”。
第二个场景:工厂函数,auto 是绳子,不是装饰
STL 里最会“返回一坨东西”的。
一直是工厂函数。
比如 make_pair。
auto p = std::make_pair(7, std::string("hi"));
你当然也可以写全。
但写全了没有奖。
而且你一旦把右边替换成别的工厂。
左边那行“手抄类型”就可能变成 bug 的温床。
更现实的版本是 make_shared。
auto sp = std::make_shared<std::string>("hello");
这里用 auto。
我一点心理负担都没有。
因为右边已经把语义说得很明白了。
“这是共享所有权”。
左边不需要再复读一遍。
但别把 auto 当成“没有类型”
auto 不是取消类型。
它只是把“把类型写出来”这件事。
交给编译器。
类型依然在那里。
规则也依然在那里。
你不懂规则。
auto 会用一种很温柔的方式。
把你送进坑里。
常见坑一:auto 会把引用和 const 悄悄抹掉
这段代码。
我在 code review 里见过很多次。
int x = 10;
int& r = x;
auto a = r;
a = 20;
你以为 a 也是引用。
其实不是。
a 是一个新的 int。
它从 r 里拷贝了一个值出来。
所以你改 a。
x 不会动。
你想“跟着引用走”。
得把话说清楚。
int x = 10;
int& r = x;
auto& a = r;
a = 20;
这次 a 才是引用。
你改它。
x 跟着变。
const 也一样。
const int cx = 42;
auto v = cx;
v 是 int。
不是 const int。
这不是编译器坏。
这是 auto 的默认性格。
它更倾向于给你一个“可用的值”。
你真想保留约束。
也得说清楚。
const int cx = 42;
const auto v = cx;
常见坑二:范围 for 里,auto 一不小心就开始复制
这坑特别隐蔽。
因为代码看起来很正常。
std::vector<std::string> names{"alice", "bob"};
for (auto name : names) {
name += "!";
}
你以为你在改容器里的字符串。
其实你只是在改副本。
auto name 推出来的是一个值。
每次循环都复制一份。
如果字符串很长。
你等于在循环里“搬家”。
想改原始元素。
就用引用。
for (auto& name : names) {
name += "!";
}
如果你不想改。
只想读。
那就把意图写到代码上。
for (const auto& name : names) {
// read only
}
这三种写法。
差的不是“语法”。
差的是“成本”。
差的是“意图”。
常见坑三:花括号会把 auto 带到另一个世界
这段代码。
很多人第一次见会愣一下。
auto xs = {1, 2, 3};
xs 不是 std::vector<int>。
它是 std::initializer_list<int>。
这不是编译器跟你作对。
是 C++11 的统一初始化规则在起作用。
你写了花括号。
你就在暗示。
“我可能在做初始化列表”。
如果你只想要一个 int。
就别绕弯。
auto x = 1;
initializer_list 很轻。
也很“窄”。
它像一张临时票。
拿来过闸机没问题。
你别拿它当长期通行证。
我这些年对 auto 的一个原则(带代码的那种)
我见过两种极端。
一种是。
全项目拒绝 auto。
理由是“看不见类型”。
另一种是。
全项目都是 auto。
连 auto i = 0; 都要写。
读起来像猜谜。
我更喜欢一个老派的折中。
auto 不是信仰。
它是一把刀。
你用得好。
切掉的是噪音。
你用得不好。
切掉的是线索。
我通常只问一个问题。
“右边有没有把类型和语义说清楚?”
如果右边已经说清楚了。
左边就别复读。
比如。
auto sp = std::make_shared<std::string>("hello");
你一眼就知道。
这是 shared_ptr。
所有权语义写在函数名里。
这时 auto 是减负。
不是隐瞒。
再比如。
auto it = v.begin();
你关心的是“迭代”。
不是那串很长的嵌套类型。
auto 让实现细节退后一步。
这很值。
但如果右边没说清楚。
我就会警惕。
auto conn = get_connection();
这里的 conn。
可能是裸指针。
可能是引用。
可能是一个 RAII 句柄。
也可能是个临时对象。
你光看这行。
很难判断它的生命周期和所有权。
这就不再是“省事”。
这叫“欠债”。
我的习惯是。
要么把类型写出来。
要么把右边写得更诚实。
比如你真的想表达“这是 RAII 句柄”。
那就让类型出来见人。
Connection conn = get_connection();
再举一个更常见的。
当类型本身就是语义的一部分。
我会更克制。
比如时间。
auto timeout = 500;
500 什么。
毫秒。
秒。
还是“随便写个数,后面再说”。
这行代码在未来的某个深夜。
很可能会背刺你。
同样的意思。
你把单位写出来。
整个团队都轻松。
std::chrono::milliseconds timeout(500);
你看。
类型在这行里不是噪音。
它就是语义。
所以我的原则从来不是“多用 auto”或“少用 auto”。
而是。
当类型在左边只是重复。
auto 就是好工具。
当类型在左边是线索。
那就别把线索藏起来。
读者不是编译器。
读者需要提示。
代码是写给人看的。
编译器只是顺便。
你帮读者省一次脑力。
他就少骂一次 C++。
这其实也是 auto 在 C++11 里翻身的原因。