(基于问题 cmets 中的讨论)
注意:我参考了 C++17 或接近答案。 C++14 的工作方式相同,文本差异在答案末尾附近注明。
void() 是一个特殊的例外。见 N4618 5.2.3 [expr.type.conv],强调我的:
1 simple-type-specifier (7.1.7.2) 或 typename-specifier (14.6) 后跟括号可选的 expression-list 或由 braced-init-list(初始化程序)构造一个给定初始化程序的指定类型的值。如果该类型是推导类类型的占位符,则在本节的其余部分中,将其替换为由重载决议选择的用于类模板推导 (13.3.1.8) 的函数的返回类型。
2 如果初始值设定项是带括号的单个表达式,则类型转换表达式(在定义上,如果在含义上定义)等价于相应的强制转换表达式(5.4)。 如果类型是(可能是 cv 限定的)void 并且初始化程序是 (),则表达式是指定类型的纯右值,不执行初始化。 否则,表达式是指定类型的纯右值其结果对象使用初始化程序直接初始化(8.6)。对于 T() 形式的表达式,T 不能是数组类型。
所以void() 之所以有效,是因为它在[expr.type.conv]/2 中明确标识为没有初始化。 void{} 不满足该异常,因此它尝试成为 直接初始化 对象。
tl;dr 此处通过 C++14 注释:您不能直接初始化 void 对象。
通过 N4618 8.6 [dcl.init] 打洞,看看 void{} 的实际情况
- 8.6/16 => 直接初始化
- 8.6/17.1 => 列表初始化,跳转到8.6.4
- 8.6.4/3.10 => 值初始化
-
void() 将使用 8.6/11 => value-initialized 将上述三个快捷方式,然后重新加入跟踪,这就是 [expr.type.conv] 是必需的。
- 8.6/8.4 => 零初始化
现在,8.6/6 定义了零初始化:
N4618 3.9 [basic.types]/9 定义标量:
算术类型 (3.9.1)、枚举类型、指针类型、指向成员类型的指针 (3.9.2)、std::nullptr_t,以及这些类型的 cv 限定版本 (3.9. 3) 统称为标量类型。
N4618 3.9.1 [basic.fundamental]/8 定义了算术类型:
整数和浮点类型统称为算术类型。
所以void不是算术类型,所以不是标量,所以不能零初始化,所以不能值初始化,所以不能直接初始化,所以表达式无效。
除了初始化之外,void() 和 void{} 将以相同的方式工作,生成 void 类型的 prvalue 表达式。即使您不能为不完整类型提供 result 对象,并且 void总是不完整:
N4618 3.9.1 [basic.fundamental]/9(粗体字):
类型 cv void 是不完整的类型,无法完成;这种类型有一组空值。
decltype 特别允许不完整类型:
N4618 7.1.7.2 [decl.type.simple]/5(粗体字):
如果 decltype-specifier 的操作数是纯右值,则不应用临时实现转换 (4.4) 并且不为纯右值提供结果对象。 纯右值的类型可能不完整。
在 C++14 中,N4296 5.2.3 [expr.type.conv] 的措辞不同。括号形式几乎是对括号版本的事后考虑:
简单类型说明符 (7.1.6.2) 或类型名称说明符 (14.6) 后跟带括号的表达式列表 结构给定表达式列表的指定类型的值。如果表达式列表是单个表达式,则类型转换表达式(在定义上,如果在含义上定义)等价于相应的强制转换表达式(5.4)。如果指定的类型是类类型,则类类型应是完整的。如果表达式列表指定了多个值,则类型应为具有适当声明的构造函数(8.5,12.1)的类,并且表达式T(x1, x2, ...) 等效于声明T t(x1, x2, ...);对于一些发明的临时变量 t,其结果是 t 的值作为纯右值。
表达式 T(),其中 T 是非数组完整对象类型的 simple-type-specifier 或 typename-specifier 或(可能是 cv 限定的)void 类型,创建指定类型的纯右值,其value 是通过值初始化 (8.5) 类型 T 的对象产生的;没有对 void() 情况进行初始化。 [注意: 如果T 是一个非类类型,它是 cv 限定的,则在确定结果纯右值的类型时,cv 限定符会被丢弃(第 5 条) )。 ——尾注]
类似地,simple-type-specifier 或 typename-specifier 后跟 braced-init-list 会创建一个临时对象使用指定的 braced-init-list 指定类型的直接列表初始化 (8.5.4),其值是作为纯右值的临时对象。
效果与我们的目的相同,change 与 P0135R1 Wording for guaranteed copy elision through simplified value categories 相关,后者从表达式中删除了临时对象创建。相反,如果上下文需要,表达式的上下文会提供由表达式初始化的结果对象。
如上所述,decltype(与sizeof 或typeid 不同)不为表达式提供结果对象,这就是void() 即使无法初始化结果对象也能工作的原因。
我觉得 N4618 5.2.3 [expr.type.conv] 中的异常也应该应用于void{}。这意味着围绕{} 的准则变得更加复杂。例如,参见 C++ 核心指南中的 ES.23: Prefer the {} initializer syntax,目前推荐 decltype(void{}) 而不是 decltype(void())。 decltype(T{}) 更有可能是这个咬你的地方...