Inisialisasi dalam C ++ modern


Telah diketahui bahwa semantik inisialisasi adalah salah satu bagian paling kompleks dari C ++. Ada banyak jenis inisialisasi, dijelaskan oleh sintaks yang berbeda, dan mereka semua berinteraksi dengan cara yang kompleks dan menantang. C ++ 11 mengusung konsep "inisialisasi universal". Sayangnya, dia memperkenalkan aturan yang lebih kompleks, dan pada gilirannya, mereka diblokir di C ++ 14, C ++ 17 dan diubah lagi di C ++ 20.


Di bawah cut - video dan terjemahan laporan Timur Doumler dari konferensi C ++ Rusia . Timur pertama-tama merangkum hasil historis dari evolusi inisialisasi dalam C ++, memberikan tinjauan sistematis tentang versi saat ini dari aturan inisialisasi, masalah khas dan kejutan, menjelaskan bagaimana menggunakan semua aturan ini secara efektif, dan akhirnya berbicara tentang proposal baru dalam standar yang dapat membuat inisialisasi semantik. C ++ 20 sedikit lebih nyaman. Selanjutnya ceritanya adalah atas namanya.



Daftar isi




Gif yang Anda lihat sekarang menyampaikan pesan utama laporan dengan sangat baik. Saya menemukannya di Internet sekitar enam bulan yang lalu, dan mempostingnya di Twitter saya. Dalam komentarnya, seseorang mengatakan bahwa tiga jenis inisialisasi tidak ada. Sebuah diskusi dimulai, di mana saya diundang untuk melaporkan hal ini. Dan semuanya dimulai.


Tentang inisialisasi Nikolay Yossutis sudah diceritakan . Laporannya termasuk slide daftar 19 cara berbeda untuk menginisialisasi sebuah int:


int i1; //undefined value int i2 = 42; //note: inits with 42 int i3(42); //inits with 42 int i4 = int(); //inits with 42 int i5{42}; //inits with 42 int i6 = {42}; //inits with 42 int i7{}; //inits with 0 int i8 = {}; //inits with 0 auto i9 = 42; //inits with 42 auto i10{42}; //C++11: std::initializer_list<int>, C++14: int auto i11 = {42}; //inits std::initializer_list<int> with 42 auto i12 = int{42}; //inits int with 42 int i13(); //declares a function int i14(7, 9); //compile-time error int i15 = (7, 9); //OK, inits int with 9 (comma operator) int i16 = int(7, 9); //compile-time error int i17(7, 9); //compile-time error auto i18 = (7, 9); //OK, inits int with 9 (comma operator) auto i19 = int(7, 9); //compile-time error 

Menurut saya ini adalah situasi unik untuk bahasa pemrograman. Menginisialisasi variabel adalah salah satu tindakan paling sederhana, tetapi dalam C ++ itu sama sekali tidak mudah dilakukan. Tidak mungkin bahwa bahasa ini memiliki bidang lain di mana dalam beberapa tahun terakhir akan ada banyak laporan penyimpangan dari standar, koreksi dan perubahan. Aturan inisialisasi berubah dari standar ke standar, dan ada banyak posting di Internet tentang bagaimana menginisialisasi dalam C ++. Karena itu, membuat tinjauan sistematis tentang hal itu adalah tugas yang tidak sepele.


Saya akan menyajikan materi dalam urutan kronologis: pertama kita akan berbicara tentang apa yang diwarisi dari C, kemudian tentang C ++ 98, kemudian tentang C ++ 03, C ++ 11, C ++ 14 dan C ++ 17. Kami akan membahas kesalahan umum, dan saya akan memberikan rekomendasi saya mengenai inisialisasi yang tepat. Saya juga akan berbicara tentang inovasi di C ++ 20. Tabel ikhtisar akan disajikan di bagian paling akhir laporan.



Inisialisasi Default (C)


Dalam C ++, banyak hal yang diwarisi dari C, itu sebabnya kita akan memulainya. Ada beberapa cara untuk menginisialisasi variabel dalam C. Mereka mungkin tidak diinisialisasi sama sekali, dan ini disebut inisialisasi default . Menurut pendapat saya, ini adalah nama yang tidak menguntungkan. Faktanya adalah bahwa tidak ada variabel yang diberi nilai default, itu hanya tidak diinisialisasi. Jika Anda beralih ke variabel yang tidak diinisialisasi dalam C ++ dan C, Anda mendapatkan perilaku yang tidak terdefinisi:


 int main() { int i; return i; // undefined behaviour } 

Hal yang sama berlaku untuk tipe khusus: jika dalam beberapa struct ada bidang yang tidak diinisialisasi, maka ketika mengaksesnya, perilaku tidak terdefinisi juga terjadi:


 struct Widget { int i; int j; }; int main() { Widget widget; return widget.i; //   } 

Banyak konstruksi baru telah ditambahkan ke C ++: kelas, konstruktor, publik, privat, metode, tetapi tidak ada yang mempengaruhi perilaku yang baru saja dijelaskan. Jika beberapa elemen tidak diinisialisasi di kelas, maka ketika mengaksesnya, perilaku tidak terdefinisi terjadi:


 class Widget { public: Widget() {} int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i; int j; }; int main() { Widget widget; return widget.get_i(); // Undefined behaviour! } 

Tidak ada cara ajaib untuk menginisialisasi elemen kelas di C ++ secara default. Ini adalah poin yang menarik, dan selama beberapa tahun pertama karir saya dengan C ++, saya tidak tahu ini. Baik kompiler maupun IDE, yang saya gunakan saat itu, mengingatkan saya akan hal ini. Kolega saya tidak memperhatikan fitur ini ketika memeriksa kode. Saya cukup yakin bahwa karena dia, ada beberapa bug yang cukup aneh dalam kode saya yang ditulis selama tahun-tahun ini. Tampak jelas bagi saya bahwa kelas harus menginisialisasi variabel mereka.


Dalam C ++ 98, Anda dapat menginisialisasi variabel menggunakan daftar penginisialisasi anggota. Tetapi solusi untuk masalah seperti itu tidak optimal, karena harus dilakukan di setiap konstruktor, dan ini mudah dilupakan. Selain itu, inisialisasi hasil dalam urutan di mana variabel dinyatakan, dan tidak dalam urutan daftar penginisialisasi anggota:


 // C++98: member initialiser list class Widget { public: Widget() : i(0), j(0) {} // member initialiser list int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i; int j; }; int main() { Widget widget; return widget.get_i(); } 

Dalam C ++ 11, inisialisasi anggota langsung ditambahkan, yang jauh lebih nyaman untuk digunakan. Mereka memungkinkan Anda untuk menginisialisasi semua variabel secara bersamaan, dan ini memberi keyakinan bahwa semua elemen diinisialisasi:


 // C++11: default member initialisers class Widget { public: Widget() {} int get_i() const noexcept { return i; } int get_j() const noexcept { return j; } private: int i = 0; // default member initialisers int j = 0; }; int main() { Widget widget; return widget.get_i(); } 

Rekomendasi pertama saya: kapan pun Anda bisa, selalu gunakan DMI (inisialisasi anggota langsung). Mereka dapat digunakan baik dengan tipe float ( float dan int ), dan dengan objek. Kebiasaan menginisialisasi elemen membuat kita mendekati masalah ini secara lebih sadar.



Salin Inisialisasi (C)


Jadi, metode inisialisasi pertama yang diwarisi dari C adalah inisialisasi secara default, dan itu tidak boleh digunakan. Cara kedua adalah inisialisasi salin . Dalam hal ini, kami menunjukkan variabel dan melalui tanda sama dengan - nilainya:


 // copy initialization int main() { int i = 2; } 

Salinan inisialisasi juga digunakan ketika argumen dilewatkan ke fungsi dengan nilai, atau ketika objek dikembalikan dari fungsi dengan nilai:


 // copy initialization int square(int i) { return i * i; } 

Tanda yang sama mungkin memberi kesan bahwa suatu nilai sedang ditetapkan, tetapi ini tidak demikian. Inisialisasi salin bukan tugas nilai. Tidak akan ada apa pun tentang apropriasi dalam laporan ini.


Properti penting lain dari inisialisasi salinan: jika jenis nilai tidak cocok, urutan konversi dijalankan. Urutan konversi memiliki aturan tertentu, misalnya, ia tidak memanggil konstruktor eksplisit, karena mereka tidak mentransformasikan konstruktor. Oleh karena itu, jika Anda melakukan inisialisasi salin untuk objek yang konstruktornya ditandai sebagai eksplisit, kesalahan kompilasi terjadi:


 struct Widget { explicit Widget(int) {} }; Widget w1 = 1; // ERROR 

Selain itu, jika ada konstruktor lain yang tidak eksplisit, tetapi jenisnya lebih buruk, maka salin inisialisasi akan menyebutnya, mengabaikan konstruktor eksplisit:


 struct Widget { explicit Widget(int) {} Widget(double) {} }; Widget w1 = 1; //  Widget(double) 


Inisialisasi Agregat (C)


Jenis inisialisasi ketiga yang ingin saya bicarakan adalah inisialisasi agregat . Itu dieksekusi ketika array diinisialisasi dengan serangkaian nilai dalam kurung:


 int i[4] = {0, 1, 2, 3}; 

Jika Anda tidak menentukan ukuran array, maka itu diturunkan dari jumlah nilai yang terlampir dalam tanda kurung:


 int j[] = {0, 1, 2, 3}; // array size deduction 

Inisialisasi yang sama digunakan untuk kelas agregat, yaitu, kelas yang hanya kumpulan elemen publik (ada beberapa aturan lagi dalam definisi kelas agregat, tetapi sekarang kita tidak akan membahasnya):


 struct Widget { int i; float j; }; Widget widget = {1, 3.14159}; 

Sintaks ini bekerja bahkan dalam C dan C ++ 98, dan, dimulai dengan C ++ 11, Anda dapat melewati tanda sama dengan itu:


 Widget widget{1, 3.14159}; 

Inisialisasi agregat sebenarnya menggunakan inisialisasi salin untuk setiap elemen. Oleh karena itu, jika Anda mencoba menggunakan inisialisasi agregat (baik dengan tanda sama dan tanpa itu) untuk beberapa objek dengan konstruktor eksplisit, maka salin inisialisasi dilakukan untuk setiap objek dan kesalahan kompilasi terjadi:


 struct Widget { explicit Widget(int) {} }; struct Thingy { Widget w1, w2; }; int main() { Thingy thingy = {3, 4}; // ERROR Thingy thingy {3, 4}; // ERROR } 

Dan jika ada konstruktor lain untuk objek-objek ini, non-eksplisit, maka itu disebut, bahkan jika itu lebih cocok untuk mengetik:


 struct Widget { explicit Widget(int) {} Widget(double) {} }; struct Thingy { Widget w1, w2; }; int main() { Thingy thingy = {3, 4}; //  Widget(double) Thingy thingy {3, 4}; //  Widget(double) } 

Mari kita pertimbangkan satu lagi properti inisialisasi agregat. Pertanyaan: nilai apa yang dihasilkan program ini?


 struct Widget { int i; int j; }; int main() { Widget widget = {1}; return widget.j; } 

Teks tersembunyi

Benar, nol. Jika Anda melewatkan beberapa elemen dalam array nilai selama inisialisasi agregat, maka variabel yang sesuai diatur ke nol. Ini adalah properti yang sangat berguna, karena berkat itu tidak akan pernah ada elemen yang tidak diinisialisasi. Ini bekerja dengan kelas agregat dan dengan array:


 //     int[100] = {}; 

Properti penting lainnya dari inisialisasi agregat adalah penghilangan tanda kurung (brace elision). Menurut Anda, nilai apa yang dihasilkan program ini? Ini memiliki Widget , yang merupakan agregat dari dua nilai int , dan Thingy , agregat dari Widget dan int . Apa yang kita dapatkan jika kita memberikan dua nilai inisialisasi untuk itu: {1, 2} ?


 struct Widget { int i; int j; }; struct Thingy { Widget w; int k; }; int main() { Thingy t = {1, 2}; return tk; //   ? } 

Teks tersembunyi

Jawabannya nol. Di sini kita berhadapan dengan subagregat, yaitu, dengan kelas agregat bersarang. Kelas semacam itu dapat diinisialisasi dengan menggunakan tanda kurung bersarang, tetapi Anda dapat melewati salah satu dari pasangan tanda kurung ini. Dalam hal ini, suatu traversal rekursif dari sub-agregat dilakukan, dan {1, 2} ternyata setara dengan {{1, 2}, 0} . Diakui, properti ini tidak sepenuhnya jelas.



Inisialisasi Statis (C)


Akhirnya, inisialisasi statis juga diwarisi dari C: variabel statis selalu diinisialisasi. Ini dapat dilakukan dengan beberapa cara. Variabel statis dapat diinisialisasi dengan ekspresi konstan. Dalam hal ini, inisialisasi terjadi pada waktu kompilasi. Jika Anda tidak menetapkan nilai apa pun ke variabel, maka nilai itu diinisialisasi ke nol:


 static int i = 3; //   statit int j; //   int main() { return i + j; } 

Program ini mengembalikan 3 meskipun j tidak diinisialisasi. Jika variabel diinisialisasi bukan oleh konstanta, tetapi oleh objek, masalah dapat muncul.


Berikut adalah contoh dari perpustakaan nyata yang sedang saya kerjakan:


 static Colour red = {255, 0, 0}; 

Ada kelas Warna di dalamnya, dan warna primer (merah, hijau, biru) didefinisikan sebagai objek statis. Ini adalah tindakan yang valid, tetapi segera setelah objek statis lain muncul di inisialisasi yang red digunakan, ketidakpastian muncul karena tidak ada urutan yang kaku di mana variabel diinisialisasi. Aplikasi Anda dapat mengakses variabel yang tidak diinisialisasi, dan kemudian macet. Untungnya, dalam C ++ 11 menjadi mungkin untuk menggunakan konstruktor constexpr , dan kemudian kita berurusan dengan inisialisasi konstan. Dalam hal ini, tidak ada masalah dengan urutan inisialisasi.


Jadi, empat jenis inisialisasi diwarisi dari bahasa C: inisialisasi default, menyalin, agregat dan inisialisasi statis.



Inisialisasi Langsung (C ++ 98)


Mari kita beralih ke C ++ 98. Mungkin fitur terpenting yang membedakan C ++ dari C adalah konstruktornya. Berikut adalah contoh panggilan konstruktor:


 Widget widget(1, 2); int(3); 

Dengan menggunakan sintaks yang sama, Anda dapat menginisialisasi tipe int seperti int dan float . Sintaks ini disebut inisialisasi langsung . Itu selalu dieksekusi ketika kita memiliki argumen dalam tanda kurung.


Untuk tipe int ( int , bool , float ) tidak ada perbedaan dari inisialisasi salinan di sini. Jika kita berbicara tentang tipe pengguna, maka, tidak seperti inisialisasi salin, dengan inisialisasi langsung, Anda dapat melewati beberapa argumen. Sebenarnya, untuk kepentingan ini, inisialisasi langsung ditemukan.


Selain itu, inisialisasi langsung tidak menjalankan urutan konversi. Alih-alih, konstruktor dipanggil menggunakan resolusi kelebihan beban. Inisialisasi langsung memiliki sintaks yang sama dengan panggilan fungsi, dan menggunakan logika yang sama dengan fungsi C ++ lainnya.


Oleh karena itu, dalam situasi dengan konstruktor eksplisit, inisialisasi langsung berfungsi dengan baik, meskipun inisialisasi salin menimbulkan kesalahan:


 struct Widget { explicit Widget(int) {} }; Widget w1 = 1; //  Widget w2(1); //    

Dalam situasi dengan dua konstruktor, satu di antaranya eksplisit, dan yang kedua kurang sesuai jenisnya, yang pertama disebut dengan inisialisasi langsung, dan yang kedua disebut dengan salinan. Dalam situasi ini, mengubah sintaks akan menyebabkan panggilan ke konstruktor lain - ini sering dilupakan:


 struct Widget { explicit Widget(int) {} Widget(double) {} }; Widget w1 = 1; //  Widget(double) Widget w2(1); //  Widget(int) 

Inisialisasi langsung selalu digunakan ketika tanda kurung digunakan, termasuk ketika notasi permintaan konstruktor digunakan untuk menginisialisasi objek sementara, serta dalam ekspresi new dengan inisialisasi dalam tanda kurung dan dalam ekspresi cast :


 useWidget(Widget(1, 2)); //   auto* widget_ptr = new Widget(2, 3); // new-expression with (args) static_cast<Widget>(thingy); // cast 

Sintaks ini ada selama C ++ itu sendiri ada, dan memiliki cacat penting yang disebutkan Nikolai dalam pidato utamanya: parse yang paling menjengkelkan . Ini berarti bahwa semua yang dapat dibaca oleh kompiler sebagai deklarasi (deklarasi), ia membaca persis seperti deklarasi.


Pertimbangkan contoh di mana ada kelas Widget dan kelas Thingy , dan konstruktor Thingy yang menerima Widget :


 struct Widget {}; struct Thingy { Thingy(Widget) {} }; int main () { Thingy thingy(Widget()); } 

Sekilas, tampaknya setelah inisialisasi Thingy , Widget default dibuat diteruskan ke sana, tetapi kenyataannya, fungsi tersebut dinyatakan di sini. Kode ini mendeklarasikan fungsi yang menerima fungsi lain sebagai input, yang tidak menerima apa pun sebagai input dan mengembalikan Widget , dan fungsi pertama mengembalikan Thingy . Kode dikompilasi tanpa kesalahan, tetapi tidak mungkin kami mencari perilaku seperti itu.



Inisialisasi Nilai (C ++ 03)


Mari kita beralih ke versi selanjutnya - C ++ 03. Secara umum diterima bahwa tidak ada perubahan signifikan dalam versi ini, tetapi tidak demikian. Di C ++ 03, inisialisasi nilai muncul, di mana tanda kurung kosong ditulis:


 int main() { return int(); // UB  C++98, 0   C++03 } 

Di C ++ 98, perilaku tidak terdefinisi terjadi di sini karena inisialisasi dilakukan secara default, dan dimulai dengan C ++ 03 program ini mengembalikan nol.


Aturannya adalah ini: jika ada konstruktor default yang ditentukan pengguna, inisialisasi dengan nilai memanggil konstruktor ini, jika tidak nol dikembalikan.


Pertimbangkan situasi dengan konstruktor khusus secara lebih rinci:


 struct Widget { int i; }; Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; } 

Dalam program ini, fungsi menginisialisasi nilai untuk Widget baru dan mengembalikannya. Kami memanggil fungsi ini dan mengakses elemen i dari objek Widget . Sejak C ++ 03, nilai kembali di sini adalah nol, karena tidak ada konstruktor default yang ditentukan pengguna. Dan jika konstruktor seperti itu ada, tetapi tidak menginisialisasi i , maka kita mendapatkan perilaku yang tidak terdefinisi:


 struct Widget { Widget() {} //   int i; }; Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; //   ,  UB } 

Perlu dicatat bahwa "yang ditentukan pengguna" tidak berarti "yang ditentukan pengguna". Ini berarti bahwa pengguna harus menyediakan tubuh konstruktor, yaitu kurung kurawal. Jika dalam contoh di atas, ganti badan konstruktor dengan = default (fitur ini ditambahkan dalam C ++ 11), arti dari perubahan program. Sekarang kami memiliki konstruktor yang ditentukan oleh pengguna (yang ditentukan pengguna), tetapi tidak disediakan oleh pengguna (yang disediakan pengguna), sehingga program mengembalikan nol:


 struct Widget { Widget() = default; // user-defined,   user-provided int i; }; Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; //  0 } 

Sekarang mari kita coba untuk Widget() = default keluar dari kelas. Arti program telah berubah lagi: Widget() = default dianggap sebagai konstruktor yang disediakan pengguna jika berada di luar kelas. Program mengembalikan perilaku tidak terdefinisi lagi.


 struct Widget { Widget(); int i; }; Widget::Widget() = default; //  ,  user-provided Widget get_widget() { return Widget(); // value initialization } int main() { return get_widget().i; //    , UB } 

Ada logika tertentu: konstruktor yang didefinisikan di luar kelas dapat berada di dalam unit terjemahan lain. Kompiler mungkin tidak melihat konstruktor ini, karena mungkin dalam file .cpp lain. Oleh karena itu, kompilator tidak dapat menarik kesimpulan tentang konstruktor seperti itu, dan tidak dapat membedakan konstruktor dengan tubuh dari konstruktor dengan = default .



Inisialisasi Universal (C ++ 11)


Ada banyak perubahan yang sangat penting dalam C ++ 11. Secara khusus, inisialisasi universal (seragam) diperkenalkan, yang saya lebih suka menyebutnya "inisialisasi unicorn" karena itu hanya ajaib. Mari kita lihat mengapa dia muncul.


Seperti yang sudah Anda perhatikan, dalam C ++ ada banyak sintaks inisialisasi yang berbeda dengan perilaku yang berbeda. Parsing menjengkelkan dengan tanda kurung menyebabkan banyak ketidaknyamanan. Para pengembang juga tidak suka bahwa inisialisasi agregat hanya dapat digunakan dengan array, tetapi tidak dengan wadah seperti std::vector . Sebagai gantinya, Anda harus menjalankan .reserve dan .push_back , atau menggunakan semua jenis pustaka menyeramkan:


 //    ,  : std::vector<int> vec = {0, 1, 2, 3, 4}; //   : std::vector<int> vec; vec.reserve(5); vec.push_back(0); vec.push_back(1); vec.push_back(2); vec.push_back(3); vec.push_back(4); 

Pembuat bahasa mencoba memecahkan semua masalah ini dengan memperkenalkan sintaksis dengan kurung kurawal tetapi tanpa tanda yang sama. Diasumsikan bahwa ini akan menjadi sintaks tunggal untuk semua jenis, di mana kurung kurawal digunakan dan tidak ada masalah parse menjengkelkan. Dalam kebanyakan kasus, sintaks ini melakukan tugasnya.


Inisialisasi baru ini disebut inisialisasi daftar , dan datang dalam dua jenis: langsung dan salin. Dalam kasus pertama, hanya kurung kurawal yang digunakan, dalam kurung kurawal kedua dengan tanda sama:


 // direct-list-initialization Widget widget{1, 2}; // copy-list-initialization Widget widget = {1, 2}; 

Daftar yang digunakan untuk inisialisasi disebut braced-init-list . Penting bahwa daftar ini bukan objek, tidak memiliki tipe. Beralih ke C ++ 11 dari versi sebelumnya tidak membuat masalah dengan tipe agregat, jadi perubahan ini tidak penting. Tetapi sekarang daftar di kawat gigi memiliki fitur baru. Meskipun tidak memiliki tipe, ini dapat disembunyikan dikonversi ke std::initializer_list , itu adalah tipe baru yang khusus. Dan jika ada konstruktor yang menerima std::initializer_list sebagai input, maka konstruktor ini disebut:


 template <typename T> class vector { //... vector(std::initializer_list<T> init); //   initializer_list }; std::vector<int> vec{0, 1, 2, 3, 4}; //  ^  

Menurut saya, dari sisi komite C ++, std::initializer_list bukan solusi yang paling sukses. Dari dia lebih banyak ruginya daripada kebaikan.


Untuk mulai dengan, std::initializer_list adalah vektor ukuran tetap dengan elemen const . Artinya, ini adalah tipe, ia memiliki fungsi begin dan end yang dikembalikan iterator, ia memiliki jenis iterator sendiri, dan untuk menggunakannya, Anda harus menyertakan header khusus. Karena elemen std::initializer_list adalah const , itu tidak dapat dipindahkan, oleh karena itu, jika T dalam kode di atas adalah tipe move-only, kode tidak akan dieksekusi.


Selanjutnya, std::initializer_list adalah sebuah objek. Dengan menggunakannya, kami, pada kenyataannya, membuat dan mentransfer objek. Sebagai aturan, kompiler dapat mengoptimalkan ini, tetapi dari sudut pandang semantik, kita masih berurusan dengan objek yang tidak perlu.


Beberapa bulan yang lalu ada jajak pendapat di Twitter: jika Anda bisa kembali ke masa lalu dan menghapus sesuatu dari C ++, apa yang akan Anda hapus? Kebanyakan dari semua suara menerima persis initializer_list .


https://twitter.com/shafikyaghmour/status/1058031143935561728


, initializer_list . , .


, . , initializer_list , . :


 std::vector<int> v(3, 0); //   0, 0, 0 std::vector<int> v{3, 0}; //   3, 0 

vector int , , , — . . , initializer_list , 3 0.


:


 std::string s(48, 'a'); // "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" std::string s{48, 'a'}; // "0a" 

48 «», «0». , string initializer_list . 48 , . ASCII 48 — «0». , , , int char . . , , .


. , ? ?


 template <typename T, size_t N> auto test() { return std::vector<T>{N}; } int main () { return test<std::string, 3>().size(); } 

, — 3. string int , 1, std::vector<std::int> initializer_list . initializer_list , . string int float , , . , . , emplace , . , {} .


, .



.
— ( {a} )
( = {a} );
:


  1. «» , std::initializer_list .
    — .
  2. ,
    () .

.


1: = {a} , a ,
.


2: , {} .
, initializer_list .
Widget<int> widget{}\ ?


 template Typename<T> struct Widget { Widget(); Widget(std::initializer_list<T>); }; int main() { Widget<int> widget{}; //    ? } 

, , initializer_list , initializer_list . . , , initializer_list . , . , .


{} . , -, , Widget() = default Widget() {} — .


Widget() = default :


 struct Widget { Widget() = default; int i; }; int main() { Widget widget{}; //   (),   vexing parse return widget.i; //  0 } 

Widget() {} :


 struct Widget { Widget() {}; // user-provided  int i; }; int main() { Widget widget{}; //  ,    return widget.i; //  ,  UB } 

: , (narrowing conversions). int double , , :


 int main() { int i{2.0}; // ! } 

, double . C++11, , . :


 struct Widget { int i; int j; }; int main() { Widget widget = {1.0, 0.0}; //   ++11    C++98/03 } 

, , , , (brace elision). , , . , map . map , — :


 std::map<std::string, std::int> my_map {{"abc", 0}, {"def", 1}}; 

, . :


 std::vector<std::string> v1 {"abc", "def"}; // OK std::vector<std::string> v2 {{"abc", "def"}}; // ?? 

, , initializer_list . initializer_list , , , . , . , .


initializer_listinitializer_list , . , const char* . , string , char . . , , .


:


  • ;
  • .

. braced-init-list . :


 Widget<int> f1() { return {3, 0}; // copy-list    } void f2(Widget); f2({3, 0}); // copy-list   

, , braced-init-list . braced-init-list , .


, . StackOverflow , . , . , , :


 #include <iostream> struct A { A() {} A(const A&) {} }; struct B { B(const A&) {} }; void f(const A&) { std::cout << "A" << std::endl; } void f(const B&) { std::cout << "B" << std::endl; } int main() { A a; f( {a} ); // A f( {{a}} ); // ambiguous f( {{{a}}} ); // B f({{{{a}}}}); // no matching function } 


++14


, C++11 . , , . C++14. , .


, ++11 direct member initializers, . , direct member initializers . ++14, direct member initializers:


 struct Widget { int i = 0; int j = 0; }; Widget widget{1, 2}; //    C++14 

, auto . ++11 auto braced-init-list, std::initializer_list :


 int i = 3; // int int i(3); // int int i{3}; // int int i = {3}; // int auto i = 3; // int auto i(3); // int auto i{3}; //  ++11 — std::initializer_list<int> auto i = {3}; //  ++11 — std::initializer_list<int> 

: auto i{3} , int , std::initializer_list<int> . ++14 , auto i{3} int . , . , auto i = {3} std::initializer_list<int> . , : int , — initializer_list .


 auto i = 3; // int auto i(3); // int auto i{3}; //  ++14 — int,         auto i = {3}; //    std::initializer_list<int> 

, C++14 , , , , . , .


, ++14 :


  • , , std::initializer_list .


  • std::initializer_list move-only .


  • c , emplace make_unique .


  • , :


    • , -;
    • ;
    • auto .

  • , , .



: assert(Widget(2,3)) , assert(Widget{2,3}) . , , , . , . .



C++


, ++.


int , . . — , .


: , , std::initializer_list , direct member initializers. , .


, é . .


 struct Point { int x = 0; int y = 0; }; setPosition(Point{2, 3}); takeWidget(Widget{}); 

braced-init-list — .


 setPosition({2, 3}); takeWidget({}); 

, , . , — , . , , , , , . , , initializer_list . : , , .


:


  • = value


  • = {args} = {} :


    • std::initializer_list
    • direct member initialisation ( (args) )

  • {args} {} é


  • (args)



, (args) vexing parse. . 2013 , , auto . , : auto i; — . , :


 auto widget = Widget(2, 3); 

, . , , vexing parse:


 auto thingy = Thingy(); 

« auto» («almost always auto», AAA), ++11 ++14 , , , std::atomic<int> :


 auto count = std::atomic<int>(0); // C++11/14:  // std::atomic is neither copyable nor movable 

, atomic . , , , , . ++17 , , (guaranteed copy elision):


 auto count = std::atomic<int>(0); // C++17: OK, guaranteed copy elision 

auto . — direct member initializers. auto .


++17 CTAD (class template argument deduction). , . . , CppCon, CTAD , . , ++17 , ++11 ++14, , . , , , , .



(++20)


++20, . , , : (designated initialization):


 struct Widget { int a; int b; int c; }; int main() { Widget widget{.a = 3, .c = 7}; }; 

, . , , . , . , b .


, , , . , .


, , 99, :


  • , , . ++ , , . :


     Widget widget{.c = 7, .a = 3}; //  

    , .


  • ++ , {.ce = 7}; , {.c{.e = 7}} :


     Widget widget{.ce = 7}; //  

  • ++ , , :


     Widget widget{.a = 3, 7}; //  

  • ++ . , -, , .


     int arr[3]{.[1] = 7}; //  



C++20


++20 , . ( wg21.link/p1008 ).


++17 , , . , , , :


 struct Widget { Widget() = delete; int i; int j; }; Widget widget1; //  Widget widget2{}; //   C++17,     C++20 

, , . ++20 . , . , . , , , .


( wg21.link/p1009 ). Braced-init-list new , : , ? — , : braced-init-list new :


 double a[]{1, 2, 3}; // OK double* p = new double[]{1, 2, 3}; //   C++17,   C++20 

, ++11 braced-init-list. ++ . , .



(C++20)


, ++20 . , . ++20 : ( wg21.link/p0960 ).


 struct Widget { int i; int j; }; Widget widget(1, 2); //   C++20 

. , emplace make_unique . . : auto , : 58.11 .


 struct Widget { int i; int j; }; auto widget = Widget(1, 2); 

, :


 int arr[3](0, 1, 2); 

, : uniform 2.0. . , , , , . — initializer_list : , , — . , . , - , — . .


, . direct member initializers. auto . direct member initializers — , . , . — , .


, , . — , — . , .



, , C++ Russia 2019 Piter «Type punning in modern C++» . , ++20, , , «» ++ , .

Source: https://habr.com/ru/post/id469465/


All Articles