Tại sao C ++ 11 không hỗ trợ danh sách bộ khởi tạo được chỉ định là C99? [đóng cửa]


121

Xem xét:

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

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

Mã trên là hợp pháp trong C99, nhưng không hợp pháp trong C ++ 11.

Cái gì là cơ sở lý luận của ủy ban tiêu chuẩn để loại trừ hỗ trợ cho một tính năng tiện dụng như vậy?


10
Ban thiết kế dường như không đưa nó vào hoặc đơn giản là nó không được đưa ra trong các cuộc họp. Cần lưu ý rằng các bộ khởi tạo được chỉ định C99 không có trong bất kỳ phiên bản đặc tả C ++ nào. Các hàm tạo dường như là cấu trúc khởi tạo được ưa thích và vì lý do chính đáng: chúng đảm bảo khởi tạo đối tượng nhất quán, nếu bạn viết chúng một cách chính xác.
Robert Harvey

19
Lý luận của bạn là lạc hậu, một ngôn ngữ không cần có cơ sở lý luận cho việc không có một tính năng, nó cần cơ sở lý luận cho việc một tính năng và một tính năng mạnh mẽ. C ++ đã đủ cồng kềnh, như nó vẫn tồn tại.
Matthieu M.

42
Một lý do chính đáng (không thể giải quyết bằng các hàm tạo ngoại trừ bằng cách viết các trình bao bọc ngạc nhiên) là cho dù bạn có sử dụng C ++ hay không, thì hầu hết các API thực là C, không phải C ++ và không ít trong số chúng khiến bạn cung cấp cấu trúc mà bạn muốn thiết lập một hoặc hai trường - và không nhất thiết là trường đầu tiên - nhưng cần khởi tạo phần còn lại bằng 0. Win32 API OVERLAPPEDlà một ví dụ như vậy. Có thể viết ={.Offset=12345};sẽ làm cho mã rõ ràng hơn nhiều (và có lẽ ít bị lỗi hơn). Các ổ cắm BSD là một ví dụ tương tự.
Damon

14
Mã trong mainkhông hợp pháp C99. Nó nên đọc struct Person p = { .age = 18 };
chqrlie

14
FYI C ++ 20 sẽ hỗ trợ các trình khởi tạo được chỉ định
Andrew Tomazos

Câu trả lời:


34

C ++ có các hàm tạo. Nếu chỉ khởi tạo một thành viên là hợp lý thì điều đó có thể được thể hiện trong chương trình bằng cách triển khai một phương thức khởi tạo thích hợp. Đây là kiểu trừu tượng mà C ++ khuyến khích.

Mặt khác, tính năng khởi tạo được chỉ định thiên về việc hiển thị và giúp các thành viên dễ dàng truy cập trực tiếp bằng mã khách hàng. Điều này dẫn đến những điều như có một người 18 tuổi (năm?) Nhưng với chiều cao và cân nặng bằng không.


Nói cách khác, các trình khởi tạo được chỉ định hỗ trợ một kiểu lập trình mà các phần tử bên trong được hiển thị và khách hàng được cung cấp sự linh hoạt để quyết định cách họ muốn sử dụng kiểu.

Thay vào đó, C ++ quan tâm nhiều hơn đến việc đặt tính linh hoạt về phía người thiết kế một kiểu, vì vậy các nhà thiết kế có thể dễ dàng sử dụng một kiểu một cách chính xác và khó sử dụng sai. Đặt trình thiết kế kiểm soát cách một kiểu có thể được khởi tạo là một phần của việc này: trình thiết kế xác định các hàm tạo, trình khởi tạo trong lớp, v.v.


12
Vui lòng hiển thị liên kết tham chiếu cho những gì bạn nói là lý do khiến C ++ không có bộ khởi tạo được chỉ định. Tôi không thể nhớ mình đã từng nhìn thấy đề xuất cho nó.
Johannes Schaub - litb

20
Có phải lý do chính của việc không cung cấp một hàm tạo Personlà tác giả của nó muốn cung cấp sự linh hoạt nhất có thể cho người dùng để thiết lập và khởi tạo các thành viên? Người dùng cũng có thể viết Person p = { 0, 0, 18 };(và vì những lý do chính đáng).
Johannes Schaub - litb

7
Gần đây, một cái gì đó tương tự đã được open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3605.html chấp nhận vào thông số C ++ 14 .
Johannes Schaub - litb

4
@ JohannesSchaub-litb Tôi không nói về nguyên nhân hoàn toàn cơ học, gần đúng (tức là nó chưa được đề xuất với ủy ban). Tôi đang mô tả những gì tôi tin là yếu tố chi phối. - Personcó thiết kế rất C nên các tính năng C có thể có ý nghĩa. Tuy nhiên, C ++ có thể cho phép một thiết kế tốt hơn, điều này cũng loại bỏ sự cần thiết của các bộ khởi tạo được chỉ định. - Theo quan điểm của tôi, việc loại bỏ hạn chế đối với các trình khởi tạo trong lớp cho các tập hợp phù hợp hơn nhiều với đặc tính của C ++ hơn là các trình khởi tạo được chỉ định.
bames53,

4
Sự thay thế C ++ cho điều này có thể được đặt tên là các đối số hàm. Nhưng hiện tại, các đối số tên không chính thức tồn tại. Xem N4172 Đối số được đặt tên cho một đề xuất về điều này. Nó sẽ làm cho mã ít bị lỗi hơn và dễ đọc hơn.
David Baird

89

Vào ngày 15 tháng 7 năm 17, P0329R4 đã được chấp nhận vàostandard: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf
Điều này mang lại hỗ trợ hạn chế choBộ khởi tạo được chỉ định. Hạn chế này được mô tả như sau trong C.1.7 [diff.decl] .4, đưa ra:

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

Các khởi tạo được chỉ định sau đây, hợp lệ trong C, bị hạn chế trong C ++:

  • struct A a = { .y = 1, .x = 2 } không hợp lệ trong C ++ vì người chỉ định phải xuất hiện trong thứ tự khai báo của các thành viên dữ liệu
  • int arr[3] = { [1] = 5 } không hợp lệ trong C ++ vì không hỗ trợ khởi tạo mảng được chỉ định
  • struct B b = {.a.x = 0} không hợp lệ trong C ++ vì không thể lồng ghép các ký hiệu chỉ định
  • struct A c = {.x = 1, 2} không hợp lệ trong C ++ vì tất cả hoặc không có thành viên dữ liệu nào phải được khởi tạo bởi người chỉ định

Đối với và Boost trước đó thực sự có hỗ trợ cho Intializers được chỉ định và đã có nhiều đề xuất để thêm hỗ trợ chotiêu chuẩn, ví dụ: n4172Đề xuất của Daryle Walker để Thêm Chỉ định vào Bộ khởi tạo . Các đề xuất trích dẫn việc thực hiệnCác trình khởi tạo được chỉ định của Visual C ++, gcc và Clang tuyên bố:

Chúng tôi tin rằng những thay đổi sẽ tương đối dễ thực hiện

Nhưng ủy ban tiêu chuẩn liên tục từ chối các đề xuất như vậy , nói rõ:

EWG đã tìm thấy nhiều vấn đề khác nhau với cách tiếp cận được đề xuất và không nghĩ rằng việc thử giải quyết vấn đề này là khả thi, vì nó đã được thử nhiều lần và lần nào cũng thất bại

Những nhận xét của Ben Voigt đã giúp tôi thấy được những vấn đề không thể vượt qua với cách tiếp cận này; được:

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

Thứ tự các hàm này sẽ được gọi trong : struct X foo = {.a = (char)f(), .b = g(), .c = h()}? Đáng ngạc nhiên, trong:

Thứ tự đánh giá các biểu thức phụ trong bất kỳ trình khởi tạo nào được sắp xếp theo trình tự không xác định [ 1 ]

(Visual C ++, gcc và Clang dường như có một hành vi đã được thỏa thuận vì tất cả chúng sẽ thực hiện các lệnh gọi theo thứ tự này :)

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

Nhưng bản chất không xác định của tiêu chuẩn có nghĩa là nếu các chức năng này có bất kỳ tương tác nào, trạng thái chương trình kết quả cũng sẽ không xác định và trình biên dịch sẽ không cảnh báo bạn : Có Cách nào để Cảnh báo về việc Hoạt động sai Bộ khởi tạo được Chỉ định không?

không có yêu cầu nghiêm ngặt initializer-list 11.6.4 [dcl.init.list] 4:

Trong danh sách bộ khởi tạo của một danh sách giằng-init, các mệnh đề bộ khởi tạo, bao gồm bất kỳ điều khoản nào là kết quả của việc mở rộng gói (17.5.3), được đánh giá theo thứ tự mà chúng xuất hiện. Có nghĩa là, mọi phép tính giá trị và phụ liên kết với một mệnh đề khởi tạo nhất định được giải trình tự trước mọi phép tính giá trị và tác dụng phụ được liên kết với bất kỳ mệnh đề khởi tạo nào theo sau nó trong danh sách được phân tách bằng dấu phẩy của danh sách bộ khởi tạo.

Vì thế hỗ trợ sẽ yêu cầu điều này được thực hiện theo thứ tự:

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

Phá vỡ khả năng tương thích với trước đó triển khai.
Như đã thảo luận ở trên, vấn đề này đã được giải quyết bởi những hạn chế về Trình khởi tạo được chỉ định được chấp nhận vào. Chúng cung cấp một hành vi tiêu chuẩn hóa, đảm bảo thứ tự thực thi của các Trình khởi tạo được Chỉ định.


3
Chắc chắn, trong mã này: struct X { int c; char a; float b; }; X x = { .a = f(), .b = g(), .c = h() };cuộc gọi đến h()được thực hiện trước f()hoặc g(). Nếu định nghĩa của struct Xkhông ở gần, điều này sẽ rất đáng ngạc nhiên. Hãy nhớ rằng các biểu thức khởi tạo không nhất thiết phải có tác dụng phụ.
Ben Voigt

2
Tất nhiên, điều này không có gì mới, việc khởi tạo thành viên ctor đã có vấn đề này, nhưng nó nằm trong định nghĩa của thành viên lớp, vì vậy việc kết hợp chặt chẽ không có gì ngạc nhiên. Và các trình khởi tạo được chỉ định không thể tham chiếu đến các thành viên khác theo cách mà các trình khởi tạo thành viên ctor có thể.
Ben Voigt

2
@MattMcNabb: Không, nó không cực đoan hơn. Nhưng người ta mong đợi nhà phát triển thực hiện hàm tạo lớp biết thứ tự khai báo thành viên. Trong khi người tiêu dùng của lớp học có thể hoàn toàn là một lập trình viên khác. Vì toàn bộ điểm là cho phép khởi tạo mà không cần phải tra cứu thứ tự của các thành viên, đây có vẻ như là một lỗ hổng nghiêm trọng trong đề xuất. Vì các trình khởi tạo được chỉ định không thể tham chiếu đến đối tượng đang được xây dựng, ấn tượng đầu tiên là các biểu thức khởi tạo có thể được đánh giá trước tiên, theo thứ tự chỉ định, sau đó khởi tạo thành viên theo thứ tự khai báo. Nhưng ...
Ben Voigt

2
@JonathanMee: Chà, câu hỏi khác đã trả lời rằng ... các trình khởi tạo tổng hợp C99 không có thứ tự, vì vậy không có kỳ vọng cho các trình khởi tạo được chỉ định được đặt hàng. Các danh sách giằng-init trong C ++ ĐƯỢC sắp xếp theo thứ tự và đề xuất cho các trình khởi tạo được chỉ định sử dụng một thứ tự có thể gây ngạc nhiên (bạn không thể nhất quán với cả thứ tự từ vựng, được sử dụng cho tất cả các danh sách giằng-init và thứ tự thành viên, được sử dụng cho ctor-initializer -danh sách)
Ben Voigt

3
Jonathan: "Hỗ trợ c ++ sẽ yêu cầu điều này được thực thi theo thứ tự [...] Phá vỡ khả năng tương thích với các triển khai c99 trước đó." Tôi không hiểu cái này, xin lỗi. 1. Nếu thứ tự là không xác định trong C99, thì rõ ràng mọi thứ tự thực tế sẽ ổn, bao gồm bất kỳ lựa chọn C ++ tùy ý nào. b) Không hỗ trợ des. khởi tạo ở tất cả các loại đã phá vỡ khả năng tương thích C99 hơn nữa ...
Sz.

34

Hơi hackery nên chia sẻ cho vui thôi.

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

Và sử dụng nó như:

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

mở rộng thành:

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

Gọn gàng, tạo một lambda với một biến có tên $kiểu Tvà bạn chỉ định trực tiếp các thành viên của nó trước khi trả lại. Xấu. Tôi tự hỏi nếu có bất kỳ mối quan tâm về hiệu suất với nó.
TankorSmash

1
Trong một bản dựng được tối ưu hóa, bạn không thấy dấu vết của lambda cũng như lệnh gọi của nó. Tất cả đều được nội tuyến.
keebus

1
Tôi hoàn toàn thích câu trả lời này.
Reed

6
Chà! Thậm chí không biết $ là một tên hợp lệ.
Chris Watts

Nó được hỗ trợ bởi các trình biên dịch C cũ và vẫn hỗ trợ cho khả năng tương thích ngược.
keebus

22

Bộ khởi tạo được chỉ định hiện được bao gồm trong nội dung C ++ 20: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf nên cuối cùng chúng ta cũng có thể thấy chúng!


3
Nhưng lưu ý rằng chúng bị hạn chế: Trong C ++, hỗ trợ khởi tạo được chỉ định bị hạn chế so với chức năng tương ứng trong C. Trong C ++, bộ chỉ định cho các thành viên dữ liệu không tĩnh phải được chỉ định theo thứ tự khai báo, bộ chỉ định cho các phần tử mảng và bộ chỉ định lồng nhau thì không được hỗ trợ và các bộ khởi tạo được chỉ định và không được chỉ định không thể được trộn lẫn trong cùng một danh sách bộ khởi tạo. Điều này có nghĩa là cụ thể, bạn vẫn sẽ không thể dễ dàng tạo một bảng tra cứu có khóa enum .
Ruslan

@Ruslan: Tôi tự hỏi tại sao C ++ lại hạn chế chúng đến vậy? Tôi hiểu rằng có thể có sự nhầm lẫn về việc liệu thứ tự mà các giá trị của các mục được đánh giá và / hoặc được ghi vào cấu trúc có khớp với thứ tự mà các mục được chỉ định trong danh sách phát sinh hay thứ tự mà các thành viên xuất hiện trong cấu trúc, nhưng giải pháp cho điều đó chỉ đơn giản là nói rằng các biểu thức khởi tạo được thực thi theo trình tự tùy ý và thời gian tồn tại của đối tượng không bắt đầu cho đến khi quá trình khởi tạo hoàn tất ( &toán tử sẽ trả về địa chỉ mà đối tượng sẽ có trong suốt thời gian tồn tại của nó).
supercat

5

Hai Tính năng Core C99 mà C ++ 11 thiếu đề cập đến “Bộ khởi tạo được chỉ định và C ++”.

Tôi nghĩ rằng 'trình khởi tạo được chỉ định' liên quan đến tối ưu hóa tiềm năng. Ở đây tôi sử dụng “gcc / g ++” 5.1 làm ví dụ.

#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();
}

Chúng tôi đã biết tại thời điểm biên dịch, a_point.xlà 0, vì vậy chúng tôi có thể mong đợi rằng nó foođược tối ưu hóa thành một 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được tối ưu hóa để x == 0chỉ in .

Đối với phiên bản 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();
}

Và đây là đầu ra của mã lắp ráp được tối ưu hóa.

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   

Chúng ta có thể thấy rằng đó a_pointkhông thực sự là một giá trị hằng số thời gian biên dịch.


8
Bây giờ hãy thử constexpr point(int _x,int _y):x(_x),y(_y){}. Trình tối ưu hóa của clang ++ dường như cũng loại bỏ sự so sánh trong mã của bạn. Vì vậy, đây chỉ là vấn đề QoI.
dyp

Tôi cũng mong đợi toàn bộ đối tượng a_point sẽ được tối ưu hóa nếu nó có liên kết nội bộ. tức là đặt nó vào không gian tên ẩn danh và xem điều gì sẽ xảy ra. goo.gl/wNL0HC
Arvid

@dyp: Thậm chí chỉ có thể xác định một phương thức khởi tạo nếu kiểu nằm trong tầm kiểm soát của bạn. Bạn không thể làm điều đó, ví dụ, cho struct addrinfohoặc struct sockaddr_in, vì vậy bạn bị bỏ lại với các bài tập tách biệt với khai báo.
musiphil

2
@musiphil Ít nhất trong C ++ 14, các cấu trúc kiểu C đó có thể được thiết lập đúng cách trong một hàm constexpr dưới dạng các biến cục bộ bằng cách sử dụng phép gán, rồi trả về từ hàm đó. Ngoài ra, quan điểm của tôi là không hiển thị một triển khai thay thế của hàm tạo trong C ++ để cho phép tối ưu hóa, nhưng cho thấy rằng trình biên dịch có thể thực hiện tối ưu hóa này nếu hình thức khởi tạo khác. Nếu trình biên dịch là "đủ tốt" (tức là hỗ trợ hình thức tối ưu hóa này), thì việc bạn sử dụng ctor hoặc các trình khởi tạo được chỉ định hay thứ gì khác sẽ không liên quan.
dyp
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.