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

Why does C++11 not support designated initializer lists as C99?

本文关键字:列表 C99 不支持 C++11 为什么      更新时间:2023-10-16

考虑:

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

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

c++11 标准委员会排除对这样一个方便功能的支持的理由是什么?

17年7月15日,P0329R4被接受为c++20标准:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf
这带来了对 c99 的指定初始值设定项的有限支持。C.1.7[diff.decl].4 对此限制描述如下:

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++中无效,因为所有数据成员或没有数据成员必须由指示符初始化

对于 c++17 及更早版本,Boost 实际上支持指定初始化器,并且有许多建议添加对 c++ 标准的支持,例如:n4172 和 Daryle Walker 的向初始值设定项添加指定项的提案。这些提案引用了 Visual C++、gcc 和 Clang 中 c99 指定初始值设定项的实现,声称:

我们相信这些变化将相对容易实施

但标准委员会一再拒绝此类提案,称:

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

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

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

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

任何初始值设定项中子表达式的求值顺序都是不确定排序的 [1]

(Visual C++、gcc 和 Clang 似乎有一个商定的行为,因为他们都会按这个顺序进行调用:)

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

但是标准的不确定性质意味着,如果这些函数有任何交互,则生成的程序状态也将是不确定的,并且编译器不会警告您: 有没有办法收到有关行为异常的指定初始值设定项的警告?

C++ 确实有严格的初始值设定项列表要求 11.6.4[dcl.init.list]4:

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

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

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

破坏与以前的 c99 实现的兼容性。
如上所述,此问题已通过 c++20 中接受的指定初始值设定项的限制而规避。它们提供标准化的行为,保证指定初始值设定项的执行顺序。

有点黑客行为,所以只是为了好玩而分享。

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

并像这样使用它:

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

扩展到:

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

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

另一方面,指定的初始值设定项功能更多地是关于公开成员并使其易于直接在客户端代码中访问。这导致一个人年龄在18岁(岁?),但身高和体重为零。


换句话说,指定的初始值设定项支持公开内部的编程风格,并且客户端可以灵活地决定如何使用该类型。

C++更感兴趣的是将灵活性放在类型设计器的一侧,因此设计人员可以轻松正确使用类型,而难以正确使用类型。让设计器控制如何初始化类型是其中的一部分:设计器确定构造函数、类内初始值设定项等。

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

C++11 Lack 的两个核心 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并不是真正的编译时常量值。