为什么C ++ 11不支持指定的初始化列表作为C99?[关闭]


121

考虑:

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

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

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

什么是 标准委员会排除支持此类方便功能的理由?


10
对于设计委员会来说,加入它显然没有意义,或者根本没有在会议中提出。值得注意的是,C99指定的初始化程序不在任何 C ++规范版本中。构造函数似乎是首选的初始化构造,并且有充分的理由:如果正确编写,它们可以保证一致的对象初始化。
罗伯特·哈维

19
您的推理是落后的,一门语言不需要具有不具有功能的理由,它需要具有一个特征和强项的理由。C ++足够膨胀。
Matthieu M.

42
一个很好的理由(除非通过编写笨拙的包装程序,否则无法用构造函数解决)是,无论您是否使用C ++,大多数真正的API都是C,而不是C ++,并且很少有API使您提供要设置的结构一个或两个字段-不一定是第一个字段-但需要将其余字段零初始化。Win32 API OVERLAPPED就是这样一个例子。能够编写={.Offset=12345};将使代码更清晰(并且可能更不易出错)。BSD插槽是一个类似的示例。
达蒙

14
中的代码main不是合法的C99。它应显示为 struct Person p = { .age = 18 };
chqrlie 2015年

14
FYI C ++ 20将支持指定的初始化程序
Andrew Tomazos

Answers:


34

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

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


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

C ++更感兴趣的是将灵活性放在类型设计者的一边,因此设计人员可以使正确使用类型的操作变得容易,而错误使用则更加困难。让设计人员控制如何初始化类型是这的一部分:设计人员确定构造函数,类内初始化程序等。


12
请显示参考链接,以说明您所说的是C ++没有指定初始化程序的原因。我不记得曾经见过这个提案。
Johannes Schaub-litb 2013年

20
不提供构造函数的原因不是Person它的作者想要为用户设置和初始化成员提供最大的灵活性吗?用户也已经可以写了Person p = { 0, 0, 18 };(有充分的理由)。
Johannes Schaub-litb 2013年


4
@ JohannesSchaub-litb我不是在谈论纯粹的机械原因(即尚未向委员会提出)。我正在描述我认为是主要因素。- Person具有非常C的设计,因此C功能可能有意义。但是,C ++可能会实现更好的设计,从而消除了对指定的初始化程序的需求。—在我看来,与指定的初始化程序相比,取消对聚合的类内初始化程序的限制与C ++的精神更加一致。
bames53 2013年

4
对此的C ++替换可以命名为函数参数。但是截至目前,名称参数尚未正式存在。有关此建议,请参见N4172命名参数。这将使代码更易于出错,并且更易于阅读。
大卫·贝尔德

89

17年7月15日,P0329R4被接纳为标准:http : //www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf
这为的指定初始化程序。给出如下限制: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 ++中无效,因为全部或全部数据成员都必须由指定者初始化

对于 而早期的Boost实际上支持指定的初始化器,并且有很多建议为该初始化器添加支持。标准,例如:n4172达里尔·沃克(Daryle 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()}?令人惊讶的是

任何初始化程序中子表达式的求值顺序都是不确定的[ 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()

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


3
当然,在此代码中:struct X { int c; char a; float b; }; X x = { .a = f(), .b = g(), .c = h() };对的调用h()f()或之前执行g()。如果的定义struct X不在附近,这将非常令人惊讶。请记住,初始化表达式不必一定没有副作用。
Ben Voigt 2015年

2
当然,这并不是什么新鲜事,ctor成员初始化已经存在此问题,但是它在类成员的定义中,因此紧密耦合也就不足为奇了。并且指定的初始化程序无法像ctor成员初始化程序那样引用其他成员。
Ben Voigt

2
@MattMcNabb:不,这不是更极端。但是,人们希望实现类构造函数的开发人员知道成员声明的顺序。而该类的使用者可能完全是另一个程序员。由于整个要点是允许初始化而不必查找成员的顺序,因此这似乎是该提案中的致命缺陷。由于指定的初始值设定项无法引用正在构造的对象,因此第一印象是可以首先按指定顺序评估初始化表达式,然后按声明顺序进行成员初始化。但是...
Ben Voigt 2015年

2
@JonathanMee:好吧,另一个问题回答了... C99聚合初始化程序是无序的,因此不希望指定的初始化程序被排序。C ++ braced-init-lists是有序的,并且指定的初始值设定项的建议使用可能令人吃惊的顺序(用于所有braced-init列表的词法顺序和用于ctor-initializer的成员顺序都不能保持一致列表)
Ben Voigt,

3
乔纳森:“ c ++支持将要求此命令以破坏与以前的c99实现的兼容性的顺序执行。” 对不起,我没有这个。1.如果顺序在C99中不确定,则显然任何实际顺序都可以,包括任意C ++选择。b)不支持des。初始化程序已经完全破坏了C99的兼容性……
Sz。

34

有点骇人听闻,所以只是为了好玩而分享。

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

并像这样使用它:

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

扩展为:

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

整洁,创建一个名为$type 的变量的lambda T,并在返回之前直接分配其成员。好漂亮 我想知道是否有任何性能问题。
TankorSmash

1
在优化的构建中,您看不到lambda或其调用的任何痕迹。全部内联。
keebus

1
我绝对喜欢这个答案。
Seph Reed

6
哇。甚至都不知道$是一个有效的名称。
克里斯·沃茨

它由旧式C编译器支持,并一直保持向后兼容性。
keebus

22

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


3
但是请注意,它们是受限制的:与C中的相应功能相比,在C ++中,指定的初始化支持受到限制。在C ++中,必须以声明顺序指定用于非静态数据成员的标识符,而不是用于数组元素和嵌套标识符的标识符。支持,并且指定的初始化程序和未指定的初始化程序不能在同一初始化程序列表中混合使用。特别是这意味着,您仍然无法轻松创建一个枚举键的查找表
罗斯兰

@Ruslan:我想知道为什么C ++限制了它们太多?我了解,对于项目值的评估和/或写入结构的顺序是否与在初始化列表中指定项目的顺序或成员在结构中出现的顺序是否匹配可能存在混淆,但是解决方案仅是说初始化表达式以任意顺序执行,并且对象的生存期直到初始化完成才开始(&操作员将返回对象在其生存期内拥有的地址)。
超级猫

5

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实际上不是一个编译时间常数值。


8
现在请尝试constexpr point(int _x,int _y):x(_x),y(_y){}。clang ++的优化器似乎也消除了代码中的比较。因此,这只是一个QoI问题。
dyp

我还希望整个a_point对象具有内部链接都可以被优化。即把它放在匿名名称空间中,看看会发生什么。goo.gl/wNL0HC
Arvid

@dyp:仅在类型受您控制的情况下,甚至可以仅定义构造函数。例如,对于struct addrinfostruct sockaddr_in,您无法执行此操作,因此剩下的工作与声明分开。
musiphil '17

2
@musiphil至少在C ++ 14中,可以通过使用赋值在constexpr函数中将这些C样式结构正确地设置为局部变量,然后从该函数返回。另外,我的观点不是要展示允许优化的C ++构造函数的替代实现,而是要说明,如果初始化形式不同,则编译器可以执行此优化。如果编译器“足够好”(即支持这种形式的优化),则是否使用ctor或指定的初始化程序或其他方法都无关紧要。
dyp '17
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.