亲爱的读者,若您还记得第9章那个有趣的章节“零的定义”,那就再好不过了。因为我们即将再次探讨这个极具影响力的数字。若您一时想不起来,也无需担心,大可随时翻回第9章重温(就当是温故知新吧)。
现在请看以下程序:
#include <iostream>
#include <typeinfo>
#include <string>
template<typename T> std::string typeof(T t) {
std::string res = typeid(t).name();
return res;
}
int main() {
auto a1 = 0;
auto a2(0);
auto a3 {0};
auto a4 = {0};
std::cout << typeof(a1) << std::endl
<< typeof(a2) << std::endl
<< typeof(a3) << std::endl
<< typeof(a4) << std::endl;
}
这段程序依然不算复杂,巧妙地运用了auto关键字,并通过前文章节介绍的各种机制将变量初始化为0。若还不清楚auto的用途,这里简要说明:C++11中的auto关键字是从C语言"征用"而来的,其新使命是实现自动类型推断。让编译器能根据初始化表达式推导变量类型。这种设计通过消除显式类型声明的需求,不仅简化了代码,更让迭代器或模板类型等复杂冗长的类型处理变得简洁。
回到代码分析,经过仔细推敲我们可以得出以下结论:
auto a1 = 0;
:这是最基础的拷贝初始化,由于0是整型字面量,a1推导为int类型
auto a2(0);
:同样简单,通过直接初始化整型字面量0,a2也推导为int
auto a3 {0};
:采用列表初始化{0}时,a3仍推导为int(注意:此行为在C++17前后有变化)
auto a4 = {0};
:这里有个特殊规则:当auto遇见带等号的大括号初始化时,会优先推导为std::initializer_list
,这是C++11为统一初始化引入的特例
使用MSVC编译时,程序输出如下:
int
int
int
class std::initializer_list<int>
若使用较新版本的GCC编译,输出会相对简洁(但核心逻辑不变):
i
i
i
St16initializer_listIiE
然而这里有个陷阱。如果用5.0版本之前的GCC编译,会看到如下"惊喜"输出:
i
i
St16initializer_listIiE
St16initializer_listIiE
这可谓“向后兼容性”带来的意外“惊喜”。不过真正的救星是Clang(3.7+版本),其编译时会给出极具参考价值的提示信息:
<source>:19:13: warning: direct list initialization of a variable
with a deduced type will change meaning in a future version of Clang;
insert an '=' to avoid a change in behavior [-Wfuture-compat]
auto a3 {0};
由此可见,在C++17标准诞生前后,auto与{x}组合的语义(在这个特定场景下)发生了微妙变化。不过值得庆幸的是,早期编译器开发者早已预见这种特殊情况,给出了非常直接的警告,这“向后兼容”做得可真够别扭的,不是吗?
有了这些铺垫,以下代码无法编译的现象就不足为奇了(假设仍沿用之前的简短程序框架):
std::cout << typeof( {0} );
为什么应该这样做呢?考虑到前面的语法的所有混乱和混乱,{0}会被推断为哪种类型?它被推导出一个 int 类型吗?或者也许是 initializer_list 类型? 它会是空指针 (nullptr) 吗或者可以从数字构建的对象,如下所示:
struct D { D(int i) {} };
void fun(D d) { }
fun({0});
现在,有没有感觉之前的玩笑没那么好笑了?