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

Posted

技术标签:

【中文标题】为啥 C++11 不支持指定初始化列表作为 C99? [关闭]【英文标题】:Why does C++11 not support designated initializer lists as C99? [closed]为什么 C++11 不支持指定初始化列表作为 C99? [关闭] 【发布时间】:2013-09-11 02:26:45 【问题描述】:

考虑:

struct Person

    int height;
    int weight;
    int age;
;

int main()

    Person p  .age = 18 ;

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

c++11 标准委员会排除对如此便捷功能的支持的理由是什么?

【问题讨论】:

显然对设计委员会来说把它包括在内是没有意义的,或者它根本没有出现在会议上。值得注意的是,C99 指定的初始值设定项不在任何 C++ 规范版本中。构造函数似乎是首选的初始化构造,并且有充分的理由:如果您正确编写它们,它们可以保证一致的对象初始化。 你的推理是落后的,一种语言不需要有理由没有一个特性,它需要一个理由来拥有一个强大的特性。就目前而言,C++ 已经够臃肿了。 一个很好的理由(这不能用构造函数来解决,除非通过编写令人震惊的包装器)是,无论你是否使用 C++,大多数真正的 API 都是 C,而不是 C++,而且其中不少让你提供您想要设置一个或两个字段的结构 - 不一定是第一个 - 但需要将其余字段初始化为零。 Win32 API OVERLAPPED 就是这样一个例子。能够编写=.Offset=12345; 将使代码更清晰(并且可能更不容易出错)。 BSD 套接字是一个类似的例子。 main 中的代码不是合法的 C99。它应该是struct Person p = .age = 18 ; 仅供参考 C++20 将支持指定的初始化程序 【参考方案1】:

2017 年 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 实际上有support for Designated Intializers,并且已经有许多提议来增加对c++ 标准的支持,例如:n4172 和Daryle Walker's Proposal to Add Designation to Initializers。提案引用了 c99 在 Visual C++、gcc 和 Clang 中的指定初始化程序的实现,声称:

我们相信这些变化实施起来会相对简单

但标准委员会多次rejects such proposals,声明:

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

Ben Voigt's comments 帮助我看到了这种方法无法克服的问题;给定:

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 似乎有一致的行为,因为它们都会按此顺序进行调用:)

    h() f() g()

但是标准的不确定性意味着,如果这些函数有任何交互,则生成的程序状态也将是不确定的,编译器不会警告你:Is there a Way to Get Warned about Misbehaving Designated Initializers?

c++确实有严格的初始化列表要求 11.6.4[dcl.init.list]4:

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

所以c++ 支持需要按顺序执行:

    f() g() h()

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

【讨论】:

当然,在这段代码中:struct X int c; char a; float b; ; X x = .a = f(), .b = g(), .c = h() ;h() 的调用在f()g() 之前执行。如果struct X 的定义不在附近,这将是非常令人惊讶的。请记住,初始化表达式不必无副作用。 当然,这不是什么新鲜事,ctor成员初始化已经有这个问题了,不过是在类成员的定义中,所以紧耦合也就不足为奇了。并且指定的初始化器不能像 ctor 成员初始化器那样引用其他成员。 @MattMcNabb:不,这不是更极端。但是人们希望实现类构造函数的开发人员知道成员声明顺序。而该类的使用者可能完全是一个不同的程序员。由于重点是允许初始化而不必查找成员的顺序,这似乎是提案中的一个致命缺陷。由于指定的初始化器不能引用正在构造的对象,第一印象是初始化表达式可以首先按指定顺序求值,然后按声明顺序进行成员初始化。但是... @JonathanMee:嗯,另一个问题的答案是...... C99 聚合初始化器是无序的,因此不期望指定初始化器是有序的。 C++ 大括号初始化列表是有序的,并且指定初始化器的提议使用了一个可能令人惊讶的顺序(您不能与用于所有大括号初始化列表的词法顺序和用于 ctor-initializer 的成员顺序保持一致-列表) Jonathan:“c++ 支持需要按顺序执行 [...] 破坏与以前 c99 实现的兼容性。”这个没看懂,抱歉1. 如果 C99 中的顺序是不确定的,那么显然 any 实际顺序应该没问题,包括任意 C++ 选择。 b) 不支持des。初始化器已经有点破坏了 C99 的兼容性......【参考方案2】:

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

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


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

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

【讨论】:

请显示参考链接,说明您所说的 C++ 没有指定初始化程序的原因。我不记得曾经看过它的提案。 不为Person 提供构造函数的真正原因不是它的作者想为用户设置和初始化成员提供最大可能的灵活性吗?用户也可以写Person p = 0, 0, 18 ;(并且有充分的理由)。 open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3605.html 最近已将类似的东西纳入 C++14 规范。 @JohannesSchaub-litb 我不是在谈论纯粹机械的直接原因(即,尚未向委员会提出)。我正在描述我认为是主导因素的因素。 — Person 具有非常 C 的设计,因此 C 功能可能有意义。然而,C++ 可能实现了更好的设计,同时也消除了对指定初始化程序的需要。 — 在我看来,取消对聚合的类内初始化器的限制比指定初始化器更符合 C++ 的精神。 C++ 的替代品可以命名为函数参数。但截至目前,名称参数还没有正式存在。有关此建议,请参阅N4172 Named arguments。这将使代码不易出错且更易于阅读。【参考方案3】:

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

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

并像这样使用它:

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

扩展为:

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

【讨论】:

整洁,创建一个名为 $ 类型为 T 的变量的 lambda,并在返回之前直接分配其成员。漂亮。我想知道它是否存在任何性能问题。 在优化的构建中,您看不到 lambda 及其调用的痕迹。都是内联的。 我非常喜欢这个答案。 哇。甚至不知道 $ 是一个有效的名称。 它得到了传统 C 编译器的支持,并且为了向后兼容而保留了支持。【参考方案4】:

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

【讨论】:

但请注意它们是受限制的:在 C++ 中,与 C 中的相应功能相比,指定的初始化支持受到限制。在 C++ 中,必须在声明中指定非静态数据成员的指示符顺序,不支持数组元素的指示符和嵌套的指示符,指定的和非指定的初始化器不能混合在同一个初始化器列表中。这意味着你仍然不能easily make a an enum-keyed lookup table . @Ruslan:我想知道为什么 C++ 限制了他们这么多?我了解,对于项目的值被评估和/或写入结构的顺序是否与初始化列表中指定的项目的顺序或成员在结构中出现的顺序相匹配,可能会有混淆,但是解决方案就是说初始化表达式以任意顺序执行,并且对象的生命周期直到初始化完成才开始(& 运算符将返回对象 在其生命周期内拥有)。【参考方案5】:

Two Core C99 Features C++11 缺少提到“指定初始化程序和 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 并不是真正的编译时间常数值。

【讨论】:

现在请尝试constexpr point(int _x,int _y):x(_x),y(_y)。 clang++ 的优化器似乎也消除了代码中的比较。所以,这只是一个 QoI 问题。 如果它有内部链接,我还希望整个 a_point 对象被优化掉。即将它放在匿名命名空间中,看看会发生什么。 goo.gl/wNL0HC @dyp:仅当类型在您的控制之下时,才可以定义构造函数。例如,对于struct addrinfostruct sockaddr_in,您不能这样做,因此您的分配与声明是分开的。 @musiphil 至少在 C++14 中,这些 C 风格的结构可以通过赋值在 constexpr 函数中正确设置为局部变量,然后从该函数返回。此外,我的意思不是展示允许优化的 C++ 中构造函数的替代实现,而是展示如果初始化形式不同,编译器可以执行此优化。如果编译器“足够好”(即支持这种形式的优化),那么无论您使用 ctor 还是指定的初始化程序或其他东西都应该是无关紧要的。

以上是关于为啥 C++11 不支持指定初始化列表作为 C99? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

VS2013中C99支持的官方状态是啥?

为啥 C++ 不支持堆栈上的动态数组? [关闭]

C语言一些实用技巧

Eclipse codan 支持将 C++11 初始化列表作为函数参数

C 如何判断编译器是否支持C90 C99?

C99 中的 func() 与 func(void)