Mengapa C ++ 11 tidak mendukung daftar penginisialisasi yang ditetapkan sebagai C99? [Tutup]

121

Mempertimbangkan:

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

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

Kode di atas legal di C99, tetapi tidak legal di C ++ 11.

Apa itu alasan komite standar untuk mengecualikan dukungan untuk fitur praktis seperti itu?

xmllmx
sumber
10
Tampaknya tidak masuk akal bagi komite desain untuk memasukkannya, atau tidak muncul dalam rapat. Perlu dicatat bahwa penginisialisasi yang ditetapkan C99 tidak ada dalam versi spesifikasi C ++ mana pun. Konstruktor tampaknya menjadi konstruksi inisialisasi yang disukai, dan untuk alasan yang bagus: mereka menjamin inisialisasi objek yang konsisten, jika Anda menulisnya dengan benar.
Robert Harvey
19
Penalaran Anda terbelakang, bahasa tidak perlu memiliki alasan untuk tidak memiliki fitur, itu membutuhkan alasan untuk memiliki satu fitur dan yang kuat. C ++ cukup membengkak, seperti berdiri.
Matthieu M.
42
Alasan yang bagus (yang tidak dapat diselesaikan dengan konstruktor kecuali dengan menulis pembungkus yang memukau) adalah apakah Anda menggunakan C ++ atau tidak, sebagian besar API yang sebenarnya adalah C, bukan C ++, dan tidak sedikit dari mereka yang membuat Anda menyediakan struktur yang ingin Anda setel satu atau dua kolom - dan belum tentu yang pertama - tetapi sisanya harus diinisialisasi dengan nol. Win32 API OVERLAPPEDadalah contohnya. Mampu menulis ={.Offset=12345};akan membuat kode lebih jelas (dan mungkin tidak terlalu rentan terhadap kesalahan). Soket BSD adalah contoh serupa.
Damon
14
Kode dalam mainC99 tidak legal. Seharusnya terbaca struct Person p = { .age = 18 };
chqrlie
14
FYI C ++ 20 akan mendukung penginisialisasi yang ditunjuk
Andrew Tomazos

Jawaban:

34

C ++ memiliki konstruktor. Jika masuk akal untuk menginisialisasi hanya satu anggota maka itu dapat diekspresikan dalam program dengan menerapkan konstruktor yang sesuai. Ini adalah jenis abstraksi yang dipromosikan C ++.

Di sisi lain, fitur penginisialisasi yang ditunjuk lebih tentang mengekspos dan membuat anggota mudah diakses secara langsung dalam kode klien. Ini mengarah pada hal-hal seperti memiliki seseorang yang berusia 18 (tahun?) Tetapi dengan tinggi dan berat badan nol.


Dengan kata lain, penginisialisasi yang ditunjuk mendukung gaya pemrograman di mana internal diekspos, dan klien diberikan fleksibilitas untuk memutuskan bagaimana mereka ingin menggunakan tipe tersebut.

C ++ lebih tertarik untuk menempatkan fleksibilitas di sisi desainer dari suatu tipe, sehingga desainer dapat membuatnya mudah untuk menggunakan tipe dengan benar dan sulit untuk digunakan secara tidak benar. Menempatkan desainer untuk mengontrol bagaimana sebuah tipe dapat diinisialisasi adalah bagian dari ini: desainer menentukan konstruktor, inisialisasi dalam kelas, dll.

bames53
sumber
12
Harap tunjukkan tautan referensi untuk apa yang Anda katakan sebagai alasan C ++ tidak memiliki penginisialisasi yang ditentukan. Saya tidak ingat pernah melihat proposal untuk itu.
Johannes Schaub - litb
20
Bukankah alasan utama untuk tidak menyediakan konstruktor karena Personpembuatnya ingin memberikan fleksibilitas yang paling mungkin bagi pengguna untuk mengatur dan menginisialisasi anggota? Pengguna juga sudah bisa menulis Person p = { 0, 0, 18 };(dan untuk alasan yang bagus).
Johannes Schaub - litb
7
Sesuatu yang serupa baru-baru ini telah diterima di spesifikasi C ++ 14 oleh open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3605.html .
Johannes Schaub - litb
4
@ JohannesSchaub-litb Saya tidak sedang berbicara tentang penyebab langsung dan mekanis murni (yaitu, hal itu belum diusulkan kepada komite). Saya sedang menjelaskan apa yang saya yakini sebagai faktor yang mendominasi. - Personmemiliki desain yang sangat C sehingga fitur C mungkin masuk akal. Namun C ++ mungkin memungkinkan desain yang lebih baik yang juga meniadakan kebutuhan penginisialisasi yang ditentukan. - Dalam pandangan saya, menghapus pembatasan inisialisasi di kelas untuk agregat jauh lebih sejalan dengan etos C ++ daripada penginisialisasi yang ditunjuk.
bames53
4
Pengganti C ++ untuk ini bisa diberi nama argumen fungsi. Tapi sampai sekarang, argumen nama tidak ada secara resmi. Lihat N4172 Argumen bernama untuk proposal ini. Ini akan membuat kode tidak terlalu rentan terhadap kesalahan dan lebih mudah dibaca.
David Baird
89

Pada tanggal 15 Juli '17 P0329R4 diterima distandar: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf
Ini membawa dukungan terbatas untukInisialisasi yang Ditunjuk. Batasan ini dijelaskan sebagai berikut oleh C.1.7 [diff.decl] .4, diberikan:

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

Inisialisasi yang Ditunjuk berikut ini, yang valid di C, dibatasi di C ++:

  • struct A a = { .y = 1, .x = 2 } tidak valid di C ++ karena penunjuk harus muncul dalam urutan deklarasi anggota data
  • int arr[3] = { [1] = 5 } tidak valid di C ++ karena inisialisasi yang ditunjuk larik tidak didukung
  • struct B b = {.a.x = 0} tidak valid di C ++ karena penunjuk tidak dapat bertingkat
  • struct A c = {.x = 1, 2} tidak valid di C ++ karena semua atau tidak satu pun anggota data harus diinisialisasi oleh penunjuk

Untuk dan Boost sebelumnya sebenarnya memiliki dukungan untuk Intializers yang Ditunjuk dan ada banyak proposal untuk menambahkan dukungan kestandar, misalnya: n4172 dan Proposal Daryle Walker untuk Menambahkan Penunjukan ke Penginisialisasi . Proposal mengutip implementasiPenginisialisasi yang Ditunjuk dalam Visual C ++, gcc, dan Clang mengklaim:

Kami yakin perubahan akan relatif mudah diterapkan

Tetapi komite standar berulang kali menolak proposal tersebut , dengan menyatakan:

EWG menemukan berbagai masalah dengan pendekatan yang diusulkan, dan merasa tidak layak untuk mencoba menyelesaikan masalah tersebut, karena telah dicoba berkali-kali dan setiap kali gagal.

Komentar Ben Voigt telah membantu saya melihat masalah yang tidak dapat diatasi dengan pendekatan ini; diberikan:

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

Dalam urutan apa fungsi-fungsi ini dipanggil : struct X foo = {.a = (char)f(), .b = g(), .c = h()}? Anehnya, di:

Urutan evaluasi subekspresi di penginisialisasi apa pun diurutkan secara tidak pasti [ 1 ]

(Visual C ++, gcc , dan Clang tampaknya memiliki perilaku yang disepakati karena mereka semua akan melakukan panggilan dalam urutan ini :)

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

Tetapi sifat standar yang tidak dapat ditentukan berarti bahwa jika fungsi-fungsi ini memiliki interaksi apa pun, status program yang dihasilkan juga akan tidak dapat ditentukan, dan kompilator tidak akan memperingatkan Anda : Adakah Cara untuk Memperingatkan tentang Penginisialisasi yang Ditunjuk yang Tidak Beraturan?

memang memiliki persyaratan daftar penginisialisasi yang ketat 11.6.4 [dcl.init.list] 4:

Dalam daftar penginisialisasi dari daftar braced-init, klausa penginisialisasi, termasuk hasil dari perluasan paket (17.5.3), dievaluasi dalam urutan kemunculannya. Artinya, setiap penghitungan nilai dan efek samping yang terkait dengan klausa penginisialisasi tertentu diurutkan sebelum setiap penghitungan nilai dan efek samping yang terkait dengan klausa penginisialisasi apa pun yang mengikutinya dalam daftar daftar penginisialisasi yang dipisahkan koma.

Begitu dukungan akan meminta ini untuk dieksekusi dalam urutan:

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

Memecah kompatibilitas dengan sebelumnya implementasi.
Seperti dibahas di atas, masalah ini telah dielakkan oleh batasan pada Penginisialisasi yang Ditunjuk yang diterima. Mereka menyediakan perilaku standar, menjamin urutan eksekusi Penginisialisasi yang Ditunjuk.

Jonathan Mee
sumber
3
Tentu, dalam kode ini: struct X { int c; char a; float b; }; X x = { .a = f(), .b = g(), .c = h() };panggilan ke h()dilakukan sebelum f()atau g(). Jika definisi struct Xtidak dekat, ini akan sangat mengejutkan. Ingatlah bahwa ekspresi penginisialisasi tidak harus bebas efek samping.
Ben Voigt
2
Tentu saja, ini bukan hal baru, inisialisasi anggota ctor sudah memiliki masalah ini, tetapi ini dalam definisi anggota kelas, jadi penggandengan yang erat bukanlah kejutan. Dan penginisialisasi yang ditunjuk tidak dapat mereferensikan anggota lain seperti yang dapat dilakukan oleh penginisialisasi anggota ctor.
Ben Voigt
2
@MattMcNabb: Tidak, ini tidak lebih ekstrim. Tapi seseorang mengharapkan pengembang yang mengimplementasikan konstruktor kelas untuk mengetahui urutan deklarasi anggota. Sedangkan konsumen kelas mungkin adalah programmer yang sama sekali berbeda. Karena intinya adalah mengizinkan inisialisasi tanpa harus mencari urutan anggota, ini tampak seperti kesalahan fatal dalam proposal. Karena penginisialisasi yang ditunjuk tidak dapat mereferensikan objek yang sedang dibuat, kesan pertama adalah bahwa ekspresi inisialisasi dapat dievaluasi terlebih dahulu, dalam urutan penunjukan, kemudian inisialisasi anggota dalam urutan deklarasi. Tapi ...
Ben Voigt
2
@JonathanMee: Nah, pertanyaan lain menjawab bahwa ... Penginisialisasi agregat C99 tidak berurutan, jadi tidak ada harapan untuk penginisialisasi yang ditunjuk untuk dipesan. C ++ braced-init-list DIurutkan, dan proposal untuk penginisialisasi yang ditunjuk menggunakan urutan yang berpotensi mengejutkan (Anda tidak dapat konsisten baik dengan urutan leksikal, digunakan untuk semua daftar braced-init, dan urutan anggota, digunakan untuk ctor-initializer -daftar)
Ben Voigt
3
Jonathan: "dukungan c ++ akan mengharuskan ini untuk dijalankan dalam urutan [...] Memutuskan kompatibilitas dengan implementasi c99 sebelumnya." Saya tidak mengerti yang ini, maaf. 1. Jika pesanan tidak dapat ditentukan di C99, maka jelas semua pesanan sebenarnya harus baik-baik saja, termasuk pilihan C ++ sewenang-wenang. b) Tidak mendukung des. penginisialisasi sama sekali sudah merusak kompatibilitas C99 lebih ...
Sz.
34

Sedikit peretasan, jadi berbagi untuk kesenangan saja.

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

Dan gunakan seperti:

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

yang berkembang menjadi:

MyFunction(([&] {
 Params ${};
 $.Name = "Foo Bar", $.Age = 18;
 return $;
}()));
keebus
sumber
Neat, membuat lambda dengan variabel bernama $tipe T, dan Anda menetapkan anggotanya secara langsung sebelum mengembalikannya. Bagus. Saya ingin tahu apakah ada masalah kinerja dengannya.
TankorSmash
1
Dalam build yang dioptimalkan, Anda tidak melihat jejak lambda atau pemanggilannya. Semuanya inline.
keebus
1
Saya sangat menyukai jawaban ini.
Seph Reed
6
Wow. Bahkan tidak tahu $ adalah nama yang valid.
Chris Watts
Itu didukung oleh kompiler C lama dan dukungan tetap untuk kompatibilitas ke belakang.
keebus
22

Penginisialisasi yang ditunjuk saat ini disertakan dalam badan kerja C ++ 20: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf jadi kami mungkin akhirnya melihatnya!

SergeyA
sumber
3
Tetapi perhatikan bahwa mereka dibatasi: Dalam C ++, dukungan inisialisasi yang ditunjuk dibatasi dibandingkan dengan fungsionalitas yang sesuai di C. Dalam C ++, penunjuk untuk anggota data non-statis harus ditentukan dalam urutan deklarasi, penunjuk untuk elemen array dan penunjuk bersarang tidak didukung, dan penginisialisasi yang ditunjuk dan yang tidak ditetapkan tidak dapat dicampur dalam daftar penginisialisasi yang sama. Artinya, secara khusus, Anda masih tidak dapat dengan mudah membuat tabel pencarian dengan kunci enum .
Ruslan
@Ruslan: Saya bertanya-tanya mengapa C ++ membatasi mereka begitu banyak? Saya memahami bahwa mungkin ada kebingungan tentang apakah urutan nilai item dievaluasi dan / atau ditulis ke struct cocok dengan urutan item yang ditentukan dalam daftar initalization, atau urutan di mana anggota muncul di struct, tetapi solusi untuk itu hanya akan mengatakan bahwa ekspresi inisialisasi dijalankan dalam urutan sewenang-wenang, dan umur objek tidak dimulai sampai inisialisasi selesai ( &operator akan mengembalikan alamat yang akan dimiliki objek selama masa pakainya).
supercat
5

Dua Fitur Core C99 yang C ++ 11 Lacks menyebutkan "Designated Initializers dan C ++".

Saya pikir 'penginisialisasi yang ditunjuk' terkait dengan pengoptimalan potensial. Di sini saya menggunakan “gcc / g ++” 5.1 sebagai contoh.

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

Kami tahu pada saat kompilasi, a_point.xadalah nol, jadi kami bisa berharap bahwa foodioptimalkan menjadi satu 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"

foodioptimalkan untuk mencetak x == 0saja.

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

Dan ini adalah keluaran dari kode assemble yang dioptimalkan.

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   

Kita dapat melihat bahwa a_pointsebenarnya bukan nilai konstanta waktu kompilasi.

wcy
sumber
8
Sekarang tolong coba constexpr point(int _x,int _y):x(_x),y(_y){}. clang ++ tampaknya menghilangkan perbandingan dalam kode Anda juga. Jadi, ini hanya masalah QoI.
dyp
Saya juga mengharapkan seluruh objek a_point dioptimalkan jika memiliki hubungan internal. yaitu meletakkannya di namespace anonim dan melihat apa yang terjadi. goo.gl/wNL0HC
Arvid
@dyp: Bahkan hanya menentukan konstruktor hanya mungkin jika jenisnya ada di bawah kendali Anda. Anda tidak dapat melakukan itu, misalnya, untuk struct addrinfoatau struct sockaddr_in, jadi Anda memiliki tugas yang terpisah dari deklarasi.
musiphil
2
@musiphil Setidaknya di C ++ 14, struct C-style tersebut dapat diatur dengan benar dalam fungsi constexpr sebagai variabel lokal dengan menggunakan penugasan, lalu dikembalikan dari fungsi tersebut. Selain itu, maksud saya bukanlah untuk menunjukkan implementasi alternatif dari konstruktor di C ++ yang memungkinkan pengoptimalan, tetapi menunjukkan bahwa kompiler dapat melakukan pengoptimalan ini jika bentuk inisialisasi berbeda. Jika kompilernya "cukup baik" (yaitu mendukung bentuk pengoptimalan ini), maka kompiler tersebut seharusnya tidak relevan apakah Anda menggunakan ctor atau penginisialisasi yang ditunjuk, atau yang lainnya.
dyp