Apakah C ++ 11 Uniform Inisialisasi adalah pengganti untuk sintaks gaya lama?

172

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.

void.pointer
sumber
Ini mungkin subjek yang lebih baik dibahas di Programmers.se. Tampaknya condong ke arah sisi Subyektif Baik.
Nicol Bolas
6
@NicolBolas: Di sisi lain, jawaban Anda yang sangat bagus mungkin adalah kandidat yang sangat baik untuk tag c ++ - faq. Saya tidak berpikir kami sudah punya penjelasan untuk ini diposting sebelumnya.
Matthieu M.

Jawaban:

233

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:

vec3 GetValue()
{
  return vec3(x, y, z);
}

Mengapa saya harus mengetik vec3dua 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:

vec3 GetValue()
{
  return {x, y, z};
}

Semuanya berfungsi.

Bahkan lebih baik untuk argumen fungsi. Pertimbangkan ini:

void DoSomething(const std::string &str);

DoSomething("A string.");

Itu bekerja tanpa harus mengetikkan nama samaran, karena std::stringtahu bagaimana membangun sendiri dari yang const 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. The std::stringkonstruktor yang mengambil const char*harus mengambil panjang string jika saya hanya lulus const 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 dengan auto, kita dapat menghindari memiliki nama ketik tambahan:

DoSomething({strValue, strLen});

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 dari auto v = GetSomething();memerlukan melihat definisi GetSomething. Tetapi itu tidak berhenti autodari 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.

class Bar;

void Func()
{
  int foo(Bar());
}

Kuis pop: apa itu foo? Jika Anda menjawab "variabel", Anda salah. Ini sebenarnya prototipe dari fungsi yang mengambil sebagai parameternya fungsi yang mengembalikan Bar, dan nilai pengembalian foofungsi 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 dinamai Bar, yang artinya membuat sementara. Atau bisa juga fungsi yang tidak mengambil parameter dan mengembalikan a Bar.

Inisialisasi seragam tidak dapat diartikan sebagai prototipe fungsi:

class Bar;

void Func()
{
  int foo{Bar{}};
}

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 ++. Dengan Typename{}, 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.

int val{5.2};

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:

std::vector<int> v{100};

Apa fungsinya? Itu bisa membuat vector<int>dengan seratus item yang dibangun default. Atau bisa membuat vector<int>dengan 1 item yang nilainya 100. 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 mengambil initializer_list<int>, dan {100} dapat menjadi valid initializer_list<int>, maka itu harus .

Untuk mendapatkan konstruktor ukuran, Anda harus menggunakan ()sebagai gantinya {}.

Perhatikan bahwa jika ini adalah vectorsesuatu yang tidak dapat dikonversi ke integer, ini tidak akan terjadi. Initializer_list tidak cocok dengan konstruktor daftar initializer dari vectortipe itu, dan karenanya kompiler bebas untuk memilih dari konstruktor lain.

Nicol Bolas
sumber
11
+1 Dipaku itu. Saya menghapus jawaban saya karena jawaban Anda membahas semua poin yang sama dengan lebih detail.
R. Martinho Fernandes
21
Poin terakhir adalah mengapa saya sangat suka std::vector<int> v{100, std::reserve_tag};. Begitu pula dengan std::resize_tag. Saat ini dibutuhkan dua langkah untuk memesan ruang vektor.
Xeo
6
@NicolBolas - Dua poin: Saya pikir masalah dengan parsing menjengkelkan adalah foo (), bukan Bar (). Dengan kata lain, jika Anda melakukannya, apakah Anda tidak 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?
void.pointer
7
@RobertDailey: "jika Anda melakukannya 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 yang Bar()bisa berupa nama ketik atau nilai sementara. Itulah yang menciptakan ambiguitas bagi kompiler.
Nicol Bolas
8
unpleasant behavior- ada istilah standar baru yang perlu diingat:>
sehe
64

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:

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

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:

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

Kode di atas akan rusak ketika seseorang memutuskan bahwa DoSomething juga harus mendukung beberapa tipe string lainnya, dan menambahkan kelebihan ini:

void DoSomething(const CoolStringType& str);

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:

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

Contoh penggunaan gagasan # 2:

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

Saya pikir itu hal buruk bahwa standar baru memungkinkan orang untuk menginisialisasi Tangga seperti ini:

Stairs s {2, 4, 6};

... 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:

T var1;
T var2 (var1);

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:

T var2 {var1}; // fails if T is std::array for example
TommiT
sumber
48
Jika Anda memiliki "<banyak dan banyak kode di sini>" kode Anda akan sulit dipahami terlepas dari sintaksisnya.
kevin cline
8
Selain IMO, adalah tugas IDE Anda untuk memberi tahu Anda jenis apa yang dikembalikan (mis. Via hovering). Tentu saja jika Anda tidak menggunakan IDE, Anda menanggung beban sendiri :)
abergmeier
4
@TommiT Saya setuju dengan beberapa bagian dari apa yang Anda katakan. Namun, dalam semangat yang sama dengan debat deklarasi tipeauto 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).
lihat
5
" Tapi sintaks menggunakan kawat gigi tidak berfungsi jika tipe T adalah agregat: " Perhatikan bahwa ini adalah cacat yang dilaporkan dalam perilaku standar, bukan yang disengaja, yang diharapkan.
Nicol Bolas
5
"Sekarang, dia harus menggulir kembali ke tanda tangan fungsi" jika Anda harus menggulir, fungsi Anda terlalu besar.
Miles Rout
-3

Jika konstruktor Anda merely copy their parametersdi masing-masing variabel kelas in exactly the same orderdi 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
2
Mengapa Anda mengatakan itu bisa lebih cepat?
jbcoe
Ini salah. Tidak ada persyaratan untuk menyatakan konstruktor: struct X { int i; }; int main() { X x{42}; }. Ini juga salah, bahwa inisialisasi yang seragam dapat lebih cepat dari inisialisasi nilai.
Tim