Saya mengerti bahwa inisialisasi seragam C ++ 11 memecahkan beberapa ambiguitas sintaksis dalam bahasa tersebut, tetapi dalam banyak presentasi Bjarne Stroustrup (terutama yang selama pembicaraan GoingNative 2012), contohnya terutama menggunakan sintaks ini sekarang setiap kali ia membangun objek.
Apakah sekarang disarankan untuk menggunakan inisialisasi seragam dalam semua kasus? Apa yang harus pendekatan umum untuk fitur baru ini sejauh gaya pengkodean berjalan dan penggunaan umum? Apa beberapa alasan untuk tidak menggunakannya?
Perhatikan bahwa dalam pikiran saya, saya terutama memikirkan konstruksi objek sebagai use case saya, tetapi jika ada skenario lain yang perlu dipertimbangkan, beri tahu saya.
Jawaban:
Gaya pengkodean pada akhirnya subyektif, dan sangat tidak mungkin manfaat kinerja yang besar akan datang darinya. Tapi inilah yang akan saya katakan bahwa Anda dapatkan dari penggunaan liberal inisialisasi seragam:
Minimalkan Redundant Typenames
Pertimbangkan yang berikut ini:
Mengapa saya harus mengetik
vec3
dua kali? Apakah ada gunanya? Kompiler tahu dengan baik dan baik apa fungsi kembali. Mengapa saya tidak bisa mengatakan, "panggil konstruktor dari apa yang saya kembalikan dengan nilai-nilai ini dan kembalikan?" Dengan inisialisasi yang seragam, saya dapat:Semuanya berfungsi.
Bahkan lebih baik untuk argumen fungsi. Pertimbangkan ini:
Itu bekerja tanpa harus mengetikkan nama samaran, karena
std::string
tahu bagaimana membangun sendiri dari yangconst char*
tersirat. Itu hebat. Tetapi bagaimana jika string itu berasal, katakan RapidXML. Atau string Lua. Artinya, katakanlah saya benar-benar tahu panjang tali di depan. Thestd::string
konstruktor yang mengambilconst char*
harus mengambil panjang string jika saya hanya lulusconst char*
.Ada kelebihan beban yang membutuhkan waktu lama secara eksplisit. Tapi untuk menggunakannya, aku harus melakukan ini:
DoSomething(std::string(strValue, strLen))
. Mengapa ada nama ketik tambahan di sana? Kompiler tahu apa jenisnya. Sama seperti denganauto
, kita dapat menghindari memiliki nama ketik tambahan:Itu hanya bekerja. Tanpa nama, tidak ada masalah, tidak ada. Kompiler melakukan tugasnya, kode lebih pendek, dan semua orang senang.
Memang, ada argumen yang dibuat bahwa versi pertama (
DoSomething(std::string(strValue, strLen))
) lebih terbaca. Artinya, sudah jelas apa yang terjadi dan siapa yang melakukan apa. Itu benar, sampai batas tertentu; memahami kode berbasis inisialisasi seragam memerlukan melihat prototipe fungsi. Ini adalah alasan yang sama mengapa beberapa orang mengatakan Anda tidak boleh melewati parameter dengan referensi non-const: sehingga Anda dapat melihat di situs panggilan jika suatu nilai sedang dimodifikasi.Tetapi hal yang sama bisa dikatakan untuk
auto
; mengetahui apa yang Anda dapatkan dariauto v = GetSomething();
memerlukan melihat definisiGetSomething
. Tetapi itu tidak berhentiauto
dari digunakan dengan pengabaian yang hampir sembrono begitu Anda memiliki akses ke sana. Secara pribadi, saya pikir itu akan baik-baik saja setelah Anda terbiasa. Apalagi dengan IDE yang bagus.Never Get The Most Vexing Parse
Ini beberapa kode.
Kuis pop: apa itu
foo
? Jika Anda menjawab "variabel", Anda salah. Ini sebenarnya prototipe dari fungsi yang mengambil sebagai parameternya fungsi yang mengembalikanBar
, dan nilai pengembalianfoo
fungsi adalah int.Ini disebut C ++ "Most Vexing Parse" karena sama sekali tidak masuk akal bagi manusia. Tetapi aturan C ++ sayangnya memerlukan ini: jika mungkin dapat diartikan sebagai prototipe fungsi, maka itu akan menjadi. Masalahnya adalah
Bar()
; itu bisa menjadi salah satu dari dua hal. Itu bisa berupa tipe yang dinamaiBar
, yang artinya membuat sementara. Atau bisa juga fungsi yang tidak mengambil parameter dan mengembalikan aBar
.Inisialisasi seragam tidak dapat diartikan sebagai prototipe fungsi:
Bar{}
selalu menciptakan sementara.int foo{...}
selalu membuat variabel.Ada banyak kasus di mana Anda ingin menggunakan
Typename()
tetapi tidak bisa karena aturan parsing C ++. DenganTypename{}
, tidak ada ambiguitas.Alasan untuk tidak
Satu-satunya kekuatan nyata yang Anda berikan adalah penyempitan. Anda tidak dapat menginisialisasi nilai yang lebih kecil dengan yang lebih besar dengan inisialisasi seragam.
Itu tidak akan dikompilasi. Anda dapat melakukannya dengan inisialisasi kuno, tetapi bukan inisialisasi seragam.
Ini dilakukan sebagian untuk membuat daftar penginisialisasi benar-benar berfungsi. Kalau tidak, akan ada banyak kasus ambigu berkaitan dengan jenis daftar penginisialisasi.
Tentu saja, beberapa orang mungkin berpendapat bahwa kode seperti itu pantas untuk tidak dikompilasi. Saya pribadi setuju; penyempitan sangat berbahaya dan dapat menyebabkan perilaku yang tidak menyenangkan. Mungkin lebih baik untuk menangkap masalah tersebut sejak awal pada tahap kompiler. Paling tidak, penyempitan menunjukkan bahwa seseorang tidak berpikir terlalu keras tentang kode.
Perhatikan bahwa penyusun umumnya akan memperingatkan Anda tentang hal semacam ini jika tingkat peringatan Anda tinggi. Jadi sungguh, semua ini dilakukan adalah membuat peringatan menjadi kesalahan yang ditegakkan. Beberapa orang mungkin mengatakan bahwa Anda seharusnya tetap melakukannya;)
Ada satu alasan lain untuk tidak:
Apa fungsinya? Itu bisa membuat
vector<int>
dengan seratus item yang dibangun default. Atau bisa membuatvector<int>
dengan 1 item yang nilainya100
. Secara teori keduanya mungkin.Dalam kenyataannya, ia melakukan yang terakhir.
Mengapa? Daftar inisialisasi menggunakan sintaksis yang sama dengan inisialisasi seragam. Jadi harus ada beberapa aturan untuk menjelaskan apa yang harus dilakukan dalam kasus ambiguitas. Aturannya cukup sederhana: jika kompiler dapat menggunakan konstruktor daftar initializer dengan daftar inisialisasi brace, maka ia akan melakukannya . Karena
vector<int>
memiliki konstruktor daftar penginisialisasi yang mengambilinitializer_list<int>
, dan {100} dapat menjadi validinitializer_list<int>
, maka itu harus .Untuk mendapatkan konstruktor ukuran, Anda harus menggunakan
()
sebagai gantinya{}
.Perhatikan bahwa jika ini adalah
vector
sesuatu yang tidak dapat dikonversi ke integer, ini tidak akan terjadi. Initializer_list tidak cocok dengan konstruktor daftar initializer darivector
tipe itu, dan karenanya kompiler bebas untuk memilih dari konstruktor lain.sumber
std::vector<int> v{100, std::reserve_tag};
. Begitu pula denganstd::resize_tag
. Saat ini dibutuhkan dua langkah untuk memesan ruang vektor.int foo(10)
akan mengalami masalah yang sama? Kedua, alasan lain untuk tidak menggunakannya tampaknya lebih merupakan masalah over engineering, tetapi bagaimana jika kita membangun semua objek kita menggunakan{}
, tetapi satu hari kemudian di jalan saya menambahkan konstruktor untuk daftar inisialisasi? Sekarang semua pernyataan konstruksi saya berubah menjadi pernyataan daftar penginisialisasi. Tampaknya sangat rapuh dalam hal refactoring. Ada komentar tentang ini?int foo(10)
, apakah Anda tidak akan mengalami masalah yang sama?" Nomor 10 adalah bilangan bulat bilangan bulat, dan bilangan bulat bilangan bulat tidak pernah bisa menjadi nama ketik. Parsing menjengkelkan berasal dari fakta yangBar()
bisa berupa nama ketik atau nilai sementara. Itulah yang menciptakan ambiguitas bagi kompiler.unpleasant behavior
- ada istilah standar baru yang perlu diingat:>Saya akan tidak setuju dengan bagian jawaban Nicol Bolas Meminimalkan Redundant Typenames . Karena kode ditulis sekali dan dibaca berulang kali, kita harus berusaha meminimalkan jumlah waktu yang diperlukan untuk membaca dan memahami kode, bukan jumlah waktu yang diperlukan untuk menulis kode. Mencoba meminimalkan pengetikan berarti mencoba mengoptimalkan hal yang salah.
Lihat kode berikut:
Seseorang yang membaca kode di atas untuk pertama kalinya mungkin tidak akan langsung memahami pernyataan pengembalian, karena pada saat dia mencapai garis itu, dia akan lupa tentang jenis kembali. Sekarang, dia harus menggulir kembali ke tanda tangan fungsi atau menggunakan beberapa fitur IDE untuk melihat tipe pengembalian dan sepenuhnya memahami pernyataan pengembalian.
Dan di sini lagi, tidak mudah bagi seseorang yang membaca kode untuk pertama kalinya untuk memahami apa yang sebenarnya sedang dibangun:
Kode di atas akan rusak ketika seseorang memutuskan bahwa DoSomething juga harus mendukung beberapa tipe string lainnya, dan menambahkan kelebihan ini:
Jika CoolStringType memiliki konstruktor yang mengambil const char * dan size_t (seperti halnya std :: string), panggilan ke DoSomething ({strValue, strLen}) akan menghasilkan kesalahan ambiguitas.
Jawaban saya atas pertanyaan aktual:
Tidak, Uniform Inisialisasi tidak boleh dianggap sebagai pengganti sintaks konstruktor gaya lama.
Dan alasan saya adalah ini:
Jika dua pernyataan tidak memiliki niat yang sama, mereka seharusnya tidak terlihat sama. Ada dua jenis gagasan inisialisasi objek:
1) Ambil semua item ini dan tuangkan ke objek ini yang saya inisialisasi.
2) Bangun objek ini menggunakan argumen yang saya berikan sebagai panduan.
Contoh penggunaan gagasan # 1:
Contoh penggunaan gagasan # 2:
Saya pikir itu hal buruk bahwa standar baru memungkinkan orang untuk menginisialisasi Tangga seperti ini:
... karena itu mengaburkan makna konstruktor. Inisialisasi seperti itu terlihat seperti gagasan # 1, tetapi ternyata tidak. Itu tidak menuangkan tiga nilai yang berbeda dari ketinggian langkah ke objek, meskipun tampak seperti itu. Dan juga, yang lebih penting, jika implementasi perpustakaan Stairs seperti di atas telah diterbitkan dan programmer telah menggunakannya, dan kemudian jika perpustakaan implementor kemudian menambahkan konstruktor initializer_list ke Stairs, maka semua kode yang telah menggunakan Stairs dengan Uniform Inisialisasi Sintaks akan pecah.
Saya berpikir bahwa komunitas C ++ harus menyetujui konvensi umum tentang bagaimana Uniform Inisialisasi digunakan, yaitu seragam pada semua inisialisasi, atau, seperti saya sangat menyarankan, memisahkan dua gagasan inisialisasi ini dan dengan demikian mengklarifikasi maksud programmer ke pembaca. Kode.
SETELAH:
Inilah alasan lain mengapa Anda tidak harus menganggap Uniform Inisialisasi sebagai pengganti sintaks lama, dan mengapa Anda tidak dapat menggunakan notasi penjepit untuk semua inisialisasi:
Katakanlah, sintaks yang Anda sukai untuk membuat salinan adalah:
Sekarang Anda berpikir Anda harus mengganti semua inisialisasi dengan sintaks brace baru sehingga Anda dapat (dan kode akan terlihat) lebih konsisten. Tetapi sintaks menggunakan kawat gigi tidak berfungsi jika tipe T adalah agregat:
sumber
auto
vs eksplisit , saya berpendapat untuk keseimbangan: inisialisasi seragam menggelinding waktu yang cukup besar dalam situasi template meta-pemrograman di mana jenis biasanya cukup jelas pula. Ini akan menghindari pengulangan darn Anda yang rumit-> decltype(....)
untuk mantera misalnya untuk templat fungsi oneline sederhana (membuat saya menangis).Jika konstruktor Anda
merely copy their parameters
di masing-masing variabel kelasin exactly the same order
di mana mereka dideklarasikan di dalam kelas, maka menggunakan inisialisasi seragam pada akhirnya bisa lebih cepat (tetapi juga bisa benar-benar identik) daripada memanggil konstruktor.Jelas, ini tidak mengubah fakta bahwa Anda harus selalu mendeklarasikan konstruktor.
sumber
struct X { int i; }; int main() { X x{42}; }
. Ini juga salah, bahwa inisialisasi yang seragam dapat lebih cepat dari inisialisasi nilai.