Mengapa inisialisasi daftar (menggunakan kurung kurawal) lebih baik daripada alternatifnya?

406
MyClass a1 {a};     // clearer and less error-prone than the other three
MyClass a2 = {a};
MyClass a3 = a;
MyClass a4(a);

Mengapa?

Saya tidak dapat menemukan jawaban di SO, jadi izinkan saya menjawab pertanyaan saya sendiri.

Oleksiy
sumber
12
Kenapa tidak digunakan auto?
Mark Garcia
33
Itu benar, memang nyaman, tetapi mengurangi keterbacaan menurut pendapat saya - Saya suka melihat tipe objek saat membaca kode. Jika Anda 100% yakin jenis objeknya, mengapa menggunakan otomatis? Dan jika Anda menggunakan inisialisasi daftar (baca jawaban saya), Anda dapat memastikan bahwa itu selalu benar.
Oleksiy
103
@Oleksiy: std::map<std::string, std::vector<std::string>>::const_iteratoringin bicara dengan Anda.
Xeo
9
@Oleksiy Saya sarankan membaca GotW ini .
Rapptz
17
@ doc Saya akan mengatakan using MyContainer = std::map<std::string, std::vector<std::string>>;lebih baik (terutama karena Anda dapat templat itu!)
JAB

Jawaban:

357

Pada dasarnya menyalin dan menempel dari Bjarne Stroustrup "The C ++ Programming Language 4th Edition" :

Inisialisasi daftar tidak memungkinkan penyempitan (§.88.4.4). Itu adalah:

  • Integer tidak dapat dikonversi ke integer lain yang tidak dapat menyimpan nilainya. Misalnya, char to int diizinkan, tetapi tidak int ke char.
  • Nilai titik-mengambang tidak dapat dikonversi ke jenis titik-mengambang lain yang tidak dapat menyimpan nilainya. Sebagai contoh, float to double diperbolehkan, tetapi tidak double to float.
  • Nilai floating-point tidak dapat dikonversi ke tipe integer.
  • Nilai integer tidak dapat dikonversi ke tipe floating-point.

Contoh:

void fun(double val, int val2) {

    int x2 = val; // if val==7.9, x2 becomes 7 (bad)

    char c2 = val2; // if val2==1025, c2 becomes 1 (bad)

    int x3 {val}; // error: possible truncation (good)

    char c3 {val2}; // error: possible narrowing (good)

    char c4 {24}; // OK: 24 can be represented exactly as a char (good)

    char c5 {264}; // error (assuming 8-bit chars): 264 cannot be 
                   // represented as a char (good)

    int x4 {2.0}; // error: no double to int value conversion (good)

}

Satu- satunya situasi di mana = lebih disukai daripada {} adalah ketika menggunakan autokata kunci untuk mendapatkan jenis yang ditentukan oleh penginisialisasi.

Contoh:

auto z1 {99};   // z1 is an int
auto z2 = {99}; // z2 is std::initializer_list<int>
auto z3 = 99;   // z3 is an int

Kesimpulan

Lebih suka {} inisialisasi daripada alternatif kecuali Anda memiliki alasan kuat untuk tidak melakukannya.

Oleksiy
sumber
52
Ada juga fakta bahwa menggunakan ()bisa diuraikan sebagai deklarasi fungsi. Ini membingungkan dan tidak konsisten yang bisa Anda katakan T t(x,y,z);tetapi tidak T t(). Dan kadang-kadang, Anda yakin x, Anda bahkan tidak bisa mengatakannya T t(x);.
juanchopanza
84
Saya sangat tidak setuju dengan jawaban ini; inisialisasi bracing menjadi berantakan total ketika Anda memiliki jenis dengan ctor menerima a std::initializer_list. RedXIII menyebutkan masalah ini (dan hanya mengabaikannya), sedangkan Anda benar-benar mengabaikannya. A(5,4)dan A{5,4}dapat memanggil fungsi yang sama sekali berbeda, dan ini adalah hal yang penting untuk diketahui. Bahkan dapat mengakibatkan panggilan yang tampaknya tidak intuitif. Mengatakan bahwa Anda harus memilih {}secara default akan membuat orang salah paham tentang apa yang terjadi. Tapi ini bukan salahmu. Saya pribadi berpikir ini adalah fitur yang dipikirkan dengan sangat buruk.
user1520427
13
@ user1520427 Itulah sebabnya ada bagian " kecuali Anda memiliki alasan kuat untuk tidak ".
Oleksiy
67
Meskipun pertanyaan ini sudah lama ia memiliki beberapa hit jadi saya menambahkan ini di sini hanya untuk referensi (saya belum melihatnya di tempat lain di halaman). Dari C ++ 14 dengan Aturan baru untuk pengurangan otomatis dari bracing-init-list sekarang mungkin untuk menulis auto var{ 5 }dan akan disimpulkan inttidak lebih dari std::initializer_list<int>.
Edoardo Sparkon Dominici
13
Haha, dari semua komentar itu masih belum jelas apa yang harus dilakukan. Yang jelas adalah, bahwa spesifikasi C ++ berantakan!
DrumM
113

Sudah ada jawaban yang bagus tentang keuntungan menggunakan inisialisasi daftar, namun aturan pribadi saya adalah TIDAK untuk menggunakan kurung kurawal bila memungkinkan, tetapi sebaliknya membuatnya tergantung pada makna konseptual:

  • Jika objek yang saya buat secara konseptual memiliki nilai yang saya berikan dalam konstruktor (mis. Kontainer, POD struct, atomics, smart pointer, dll.), Maka saya menggunakan kawat gigi.
  • Jika konstruktor menyerupai pemanggilan fungsi normal (ia melakukan beberapa operasi yang lebih atau kurang kompleks yang ditentukan oleh argumen) maka saya menggunakan sintaks pemanggilan fungsi normal.
  • Untuk inisialisasi default, saya selalu menggunakan kurung kurawal.
    Untuk satu, dengan cara itu saya selalu yakin bahwa objek akan diinisialisasi terlepas dari apakah itu misalnya kelas "nyata" dengan konstruktor default yang akan dipanggil tetap atau tipe builtin / POD. Kedua itu - dalam banyak kasus - konsisten dengan aturan pertama, sebagai objek default diinisialisasi sering mewakili objek "kosong".

Dalam pengalaman saya, aturan ini dapat diterapkan jauh lebih konsisten daripada menggunakan kurung kurawal secara default, tetapi harus secara eksplisit mengingat semua pengecualian ketika mereka tidak dapat digunakan atau memiliki makna yang berbeda dari sintaks pemanggilan fungsi "normal" dengan tanda kurung (memanggil kelebihan beban yang berbeda).

Misalnya cocok dengan tipe perpustakaan standar seperti std::vector:

vector<int> a{10,20};   //Curly braces -> fills the vector with the arguments

vector<int> b(10,20);   //Parentheses -> uses arguments to parametrize some functionality,                          
vector<int> c(it1,it2); //like filling the vector with 10 integers or copying a range.

vector<int> d{};      //empty braces -> default constructs vector, which is equivalent
                      //to a vector that is filled with zero elements
MikeMB
sumber
11
Sepenuhnya setuju dengan sebagian besar jawaban Anda. Namun, tidakkah Anda berpikir menempatkan kawat gigi kosong untuk vektor hanya berlebihan? Maksudku, tidak apa-apa, ketika Anda perlu menginisialisasi nilai objek tipe T generik, tetapi apa tujuan melakukannya untuk kode non-generik?
Mikhail
8
@Mikhail: Ini memang berlebihan, tetapi merupakan kebiasaan saya untuk selalu membuat inisialisasi variabel lokal menjadi eksplisit. Seperti yang saya tulis, ini terutama tentang konsistensi jadi saya tidak melupakannya, ketika itu penting. Hal ini tentu tidak akan saya sebutkan dalam ulasan kode atau dimasukkan ke dalam panduan gaya.
MikeMB
4
aturan yang cukup bersih.
laike9m
5
Sejauh ini, inilah jawaban terbaik. {} seperti warisan - mudah disalahgunakan, menyebabkan kode sulit dipahami.
UKMonkey
2
Contoh @MikeMB: const int &b{}<- tidak mencoba membuat referensi yang tidak diinisialisasi, tetapi mengikatnya ke objek integer sementara. Contoh kedua: struct A { const int &b; A():b{} {} };<- tidak mencoba membuat referensi yang tidak diinisialisasi (seperti yang ()akan dilakukan), tetapi mengikatnya ke objek integer sementara, dan kemudian membiarkannya menggantung. GCC bahkan dengan -Walltidak memperingatkan untuk contoh kedua.
Johannes Schaub - litb
92

Ada alasan BANYAK untuk inisialisasi penggunaan brace, tetapi Anda harus menyadari bahwa yang initializer_list<>konstruktor lebih disukai untuk konstruktor lain , pengecualian menjadi default-konstruktor. Ini mengarah ke masalah dengan konstruktor dan template di mana Tkonstruktor tipe dapat berupa daftar penginisialisasi atau ctor tua yang polos.

struct Foo {
    Foo() {}

    Foo(std::initializer_list<Foo>) {
        std::cout << "initializer list" << std::endl;
    }

    Foo(const Foo&) {
        std::cout << "copy ctor" << std::endl;
    }
};

int main() {
    Foo a;
    Foo b(a); // copy ctor
    Foo c{a}; // copy ctor (init. list element) + initializer list!!!
}

Dengan asumsi Anda tidak menemukan kelas-kelas seperti itu, ada sedikit alasan untuk tidak menggunakan daftar intializer.

Merah XIII
sumber
20
Ini adalah poin yang sangat penting dalam pemrograman generik. Saat Anda menulis templat, jangan gunakan bracing-init-list (nama standar untuk { ... }) kecuali Anda menginginkan initializer_listsemantiknya (well, dan mungkin untuk membangun objek secara default).
Xeo
82
Sejujurnya saya tidak mengerti mengapa std::initializer_listaturan itu ada - itu hanya menambah kebingungan dan kekacauan pada bahasa. Apa yang salah dengan melakukan Foo{{a}}jika Anda menginginkan std::initializer_listkonstruktor? Tampaknya itu jauh lebih mudah dipahami daripada std::initializer_listlebih diutamakan daripada semua kelebihan lainnya.
user1520427
5
+1 untuk komentar di atas, karena itu benar-benar berantakan saya pikir !! itu bukan logika; Foo{{a}}mengikuti beberapa logika bagi saya jauh lebih dari Foo{a}yang berubah menjadi prioritas daftar intializer (mungkin ups pengguna berpikir hm ...)
Gabriel
32
Pada dasarnya C ++ 11 menggantikan satu kekacauan dengan kekacauan lain. Oh, maaf itu tidak menggantikannya - itu menambahnya. Bagaimana Anda bisa tahu jika Anda tidak menemukan kelas seperti itu? Bagaimana jika Anda mulai tanpa std::initializer_list<Foo> konstruktor, tetapi akan ditambahkan ke Fookelas di beberapa titik untuk memperluas antarmuka? Kemudian pengguna Fookelas kacau.
doc
10
..apakah "alasan BANYAK untuk menggunakan inisialisasi brace"? Jawaban ini menunjukkan satu alasan ( initializer_list<>), yang tidak memenuhi syarat siapa mengatakan itu lebih disukai, dan kemudian mulai menyebutkan satu kasus bagus di mana itu TIDAK disukai. Apa yang saya lewatkan itu ~ 30 orang lain (per 2016-04-21) menemukan bermanfaat?
dwanderson
0

Ini hanya lebih aman selama Anda tidak membangun dengan -Sekarang menyempit seperti kata Google di Chromium. Jika Anda melakukannya, maka itu KURANG aman. Tanpa tanda itu, hanya case yang tidak aman akan diperbaiki oleh C ++ 20 sekalipun.

Catatan: A) Kurung keriting lebih aman karena tidak memungkinkan penyempitan. B) Kurung kurawal kurang aman karena mereka dapat memotong konstruktor pribadi atau dihapus, dan memanggil konstruktor bertanda eksplisit secara implisit.

Kedua gabungan itu berarti mereka lebih aman jika apa yang ada di dalamnya adalah konstanta primitif, tetapi kurang aman jika mereka adalah objek (meskipun tetap dalam C ++ 20)

Allan Jensen
sumber
Saya mencoba mencari-cari di goldbolt.org untuk memotong konstruktor "eksplisit" atau "pribadi" menggunakan kode sampel yang disediakan dan membuat satu atau yang lain pribadi atau eksplisit, dan dihargai dengan kesalahan kompiler yang sesuai [s]. Peduli untuk mendukungnya dengan beberapa kode sampel?
Mark Storer
Ini adalah perbaikan untuk masalah yang diusulkan untuk C ++ 20: open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf
Allan Jensen
1
Jika Anda mengedit jawaban Anda untuk menunjukkan versi C ++ yang sedang Anda bicarakan, saya akan dengan senang hati mengubah suara saya.
Mark Storer