为什么 C + + 11不支持指定的初始值设定项列表为 C99?

考虑一下:

struct Person
{
int height;
int weight;
int age;
};


int main()
{
Person p { .age = 18 };
}

上面的代码在 C99中是合法的,但是在 C + + 11中是不合法的。

标准委员会为什么不支持这种方便的特性?

89977 次浏览

C + + 有构造函数。如果只初始化一个成员是有意义的,那么可以通过实现适当的构造函数在程序中表示。这是 C + + 提倡的那种抽象。

另一方面,指定的初始化器特性更多地是关于在客户端代码中公开和使成员容易直接访问。这会导致一些事情,比如一个18岁的人(年龄?)但身高和体重都为零。


换句话说,指定的初始化器支持一种编程样式,其中公开了内部元素,并且客户机可以灵活地决定如何使用该类型。

C + + 更感兴趣的是将灵活性放在类型的 设计师一侧,这样设计人员就可以使正确使用类型变得容易,而使不正确使用类型变得困难。让设计器控制如何初始化类型是其中的一部分: 设计器确定构造函数、类内初始化器等。

7月15日17日,P0329R4被接受为 标准: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf
这给 的指定初始化器带来了有限的支持:

struct A { int x, y; };
struct B { struct A a; };

以下指定初始化在 C 中有效,在 C + + 中受到限制:

  • struct A a = { .y = 1, .x = 2 }在 C + + 中无效,因为指示符必须按照数据成员的声明顺序出现
  • int arr[3] = { [1] = 5 }在 C + + 中无效,因为不支持数组指定的初始化
  • struct B b = {.a.x = 0}在 C + + 中无效,因为指示符不能嵌套
  • struct A c = {.x = 1, 2}在 C + + 中是无效的,因为所有或全部数据成员都必须由指示符初始化

对于 和更早的 Boost 实际上有 对指定初始化器的支持,并且有许多建议增加对 标准的支持,例如: N4172Daryle Walker 关于在初始化器中添加名称的建议。这些提案引用了 Visual C + + 、 gcc 和 Clang 中 指定初始化器的实现,声称:

我们相信,这些变化的实施将相对简单

但标准委员会反复强调 反对这样的提议:

EWG 发现这种方法存在各种各样的问题,并且认为尝试解决这个问题是不可行的,因为这种方法已经尝试了很多次,每次都失败了

Ben Voigt 的评论帮助我看到了这种方法无法克服的问题:

struct X {
int c;
char a;
float b;
};

: struct X foo = {.a = (char)f(), .b = g(), .c = h()}中,这些函数的调用顺序是什么:

任何初始值设定项中子表达式的求值顺序都是不确定序列的 [ < a href = “ http://en.cpferences ence.com/w/c/language/struct _ initialization # Notes”rel = “ norefrer”> 1 ]

(Visual C + + 、 GCC和 Clang 似乎有一个一致的行为,因为它们都将按照以下顺序进行调用:)

  1. h()
  2. f()
  3. g()

但是标准的不确定性意味着如果这些函数有任何交互作用,那么结果程序状态也是不确定的,编译器也不会提醒你: 有没有办法警告错误的指定初始化程序?

是的有严格的初始化器列表要求11.6.4[ dcl.init.list ]4:

在括号初始化列表的初始化器列表中,初始化器子句(包括由包扩展(17.5.3)产生的任何子句)按其出现的顺序进行计算。也就是说,与给定的初始化器子句相关联的每个值计算和副作用在每个值计算之前进行排序,与初始化器列表中的逗号分隔列表中的任何初始化器子句相关联的副作用进行排序。

因此,支持需要按照以下顺序执行:

  1. f()
  2. g()
  3. h()

破坏了与以前的 实现的兼容性。
如上所述,这个问题已经被 中接受的指定初始化器的限制所规避。它们提供了标准化的行为,保证了指定初始化器的执行顺序。

C + + 11缺少的两个核心 C99特性 提到了“指定的初始化器和 C + +”。

我认为“指定的初始化器”与潜在的优化有关,这里我使用“ gcc/g + +”5.1作为例子。

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
struct point {
int x;
int y;
};
const struct point a_point = {.x = 0, .y = 0};
int foo() {
if(a_point.x == 0){
printf("x == 0");
return 0;
}else{
printf("x == 1");
return 1;
}
}
int main(int argc, char *argv[])
{
return foo();
}

我们知道在编译时,a_point.x为零,所以我们可以预期 foo被优化为单个 printf

$ gcc -O3 a.c
$ gdb a.out
(gdb) disassemble foo
Dump of assembler code for function foo:
0x00000000004004f0 <+0>: sub    $0x8,%rsp
0x00000000004004f4 <+4>: mov    $0x4005bc,%edi
0x00000000004004f9 <+9>: xor    %eax,%eax
0x00000000004004fb <+11>:    callq  0x4003a0 <printf@plt>
0x0000000000400500 <+16>:    xor    %eax,%eax
0x0000000000400502 <+18>:    add    $0x8,%rsp
0x0000000000400506 <+22>:    retq
End of assembler dump.
(gdb) x /s 0x4005bc
0x4005bc:   "x == 0"

foo优化为只打印 x == 0

对于 C + + 版本,

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
struct point {
point(int _x,int _y):x(_x),y(_y){}
int x;
int y;
};
const struct point a_point(0,0);
int foo() {
if(a_point.x == 0){
printf("x == 0");
return 0;
}else{
printf("x == 1");
return 1;
}
}
int main(int argc, char *argv[])
{
return foo();
}

这是优化的汇编代码的输出。

g++ -O3 a.cc
$ gdb a.out
(gdb) disassemble foo
Dump of assembler code for function _Z3foov:
0x00000000004005c0 <+0>:    push   %rbx
0x00000000004005c1 <+1>:    mov    0x200489(%rip),%ebx        # 0x600a50 <_ZL7a_point>
0x00000000004005c7 <+7>:    test   %ebx,%ebx
0x00000000004005c9 <+9>:    je     0x4005e0 <_Z3foov+32>
0x00000000004005cb <+11>:   mov    $0x1,%ebx
0x00000000004005d0 <+16>:   mov    $0x4006a3,%edi
0x00000000004005d5 <+21>:   xor    %eax,%eax
0x00000000004005d7 <+23>:   callq  0x400460 <printf@plt>
0x00000000004005dc <+28>:   mov    %ebx,%eax
0x00000000004005de <+30>:   pop    %rbx
0x00000000004005df <+31>:   retq
0x00000000004005e0 <+32>:   mov    $0x40069c,%edi
0x00000000004005e5 <+37>:   xor    %eax,%eax
0x00000000004005e7 <+39>:   callq  0x400460 <printf@plt>
0x00000000004005ec <+44>:   mov    %ebx,%eax
0x00000000004005ee <+46>:   pop    %rbx
0x00000000004005ef <+47>:   retq

我们可以看到,a_point实际上不是一个编译时常量值。

指定的初始化程序目前包含在 C + + 20主体的工作: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf,所以我们可能最终看到他们!

有点恶作剧,所以只是为了好玩而分享。

#define with(T, ...)\
([&]{ T ${}; __VA_ARGS__; return $; }())

像这样使用它:

MyFunction(with(Params,
$.Name = "Foo Bar",
$.Age  = 18
));

扩展到:

MyFunction(([&] {
Params ${};
$.Name = "Foo Bar", $.Age = 18;
return $;
}()));