Tempat licin di C ++ 17

gambar

Dalam beberapa tahun terakhir, C ++ telah melangkah maju dengan cepat, dan mengikuti semua seluk-beluk dan seluk-beluk bahasa bisa sangat, sangat sulit. Sebuah standar baru tidak jauh, namun, pengenalan tren baru bukanlah proses tercepat dan termudah, oleh karena itu, sementara ada sedikit waktu sebelum C ++ 20, saya sarankan menyegarkan atau menemukan beberapa tempat "licin" standar standar saat ini. bahasa

Hari ini saya akan memberi tahu Anda mengapa jika constexpr bukan pengganti makro, apa "internal" dari penjilidan terstruktur dan "jebakan" nya dan apakah benar salin salinan selalu berfungsi sekarang dan Anda dapat menulis pengembalian tanpa ragu.

Jika Anda tidak takut kotor sedikit pun, gali "bagian dalam" lidah Anda, selamat datang di Cat.



jika constexpr


Mari kita mulai dengan yang paling sederhana - if constexpr memungkinkan Anda untuk membuang cabang ekspresi bersyarat yang kondisi yang diinginkan tidak terpenuhi bahkan pada tahap kompilasi.

Tampaknya ini adalah pengganti makro #if mematikan logika "ekstra"? Tidak. Tidak semuanya.

Pertama, if memiliki properti yang tidak tersedia untuk makro - di dalam Anda dapat menghitung ekspresi constexpr yang dapat constexpr menjadi bool . Nah, dan kedua, isi dari cabang yang dibuang harus benar secara sintaksis dan semantik.

Karena persyaratan kedua, if constexpr tidak dapat digunakan, misalnya, fungsi yang tidak ada (kode platform-dependen tidak dapat dipisahkan secara eksplisit dengan cara ini) atau buruk dari sudut pandang bahasa konstruksi (misalnya, " void T = 0; ").

Apa gunanya menggunakan if constexpr ? Poin utama ada di template. Ada aturan khusus untuk mereka: cabang yang dibuang tidak dipakai saat template dibuat dipakai. Ini membuatnya lebih mudah untuk menulis kode yang entah bagaimana tergantung pada properti dari tipe templat.

Namun, dalam templat, orang tidak boleh lupa bahwa kode di dalam cabang harus benar setidaknya untuk beberapa contoh instantiation (bahkan murni potensial), oleh karena itu, tidak static_assert(false) untuk menulis, misalnya, static_assert(false) di dalam salah satu cabang (perlu bahwa ini static_assert tergantung pada beberapa parameter yang bergantung pada template).

Contoh:

 void foo() {    //    ,       if constexpr ( os == OS::win ) {        win_api_call(); //         }    else {        some_other_os_call(); //  win      } } 

 template<class T> void foo() {    //    ,    T      if constexpr ( os == OS::win ) {        T::win_api_call(); //  T   ,    win    }    else {        T::some_other_os_call(); //  T   ,         } } 

 template<class T> void foo() {    if constexpr (condition1) {        // ...    }    else if constexpr (condition2) {        // ...    }    else {        // static_assert(false); //          static_assert(trait<T>::value); // ,   ,  trait<T>::value   false    } } 

Hal-hal yang Harus Diingat


  1. Kode di semua cabang harus benar.
  2. Di dalam templat, konten cabang yang dibuang tidak dipakai.
  3. Kode di dalam cabang apa pun harus benar untuk setidaknya satu varian instantiasi templat yang murni potensial.

Ikatan terstruktur




Di C ++ 17, mekanisme yang cukup nyaman untuk mendekomposisi berbagai objek mirip tuple muncul, memungkinkan Anda untuk dengan mudah dan ringkas mengikat elemen-elemen internal mereka ke variabel bernama:

 //     β€”    : for (const auto& [key, value] : map) {    std::cout << key << ": " << value << std::endl; } 

Dengan objek seperti tuple, maksud saya adalah objek yang jumlah elemen internalnya tersedia pada saat kompilasi diketahui (dari "tuple" - daftar terurut dengan jumlah elemen tetap (vektor)).

Definisi tersebut termasuk dalam definisi ini sebagai: std::pair , std::tuple , std::array , array dari form β€œ T a[N] ”, serta berbagai struktur dan kelas yang ditulis sendiri.

Stop ... Bisakah Anda menggunakan struktur Anda sendiri dalam pengikatan struktural? Spoiler: Anda bisa (walaupun terkadang Anda harus bekerja keras (tetapi lebih dari itu di bawah)).

Bagaimana cara kerjanya


Pekerjaan menghubungkan struktural layak mendapatkan artikel terpisah, tetapi karena kita berbicara secara khusus tentang tempat-tempat "licin", saya akan mencoba menjelaskan secara singkat bagaimana semuanya bekerja.

Standar ini menyediakan sintaks berikut untuk mendefinisikan pengikatan:

attr (opsional) cv-auto ref-operator (opsional) ekspresi [ daftar pengidentifikasi ];

  • attr - daftar atribut opsional;
  • cv-auto - otomatis dengan kemungkinan pengubah const / volatile;
  • ref-operator - specifier referensi opsional (& atau &&);
  • identifier-list - daftar nama variabel baru;
  • expression adalah ekspresi yang menghasilkan objek seperti tuple yang digunakan untuk mengikat (ekspresi bisa dalam bentuk " = expr ", " {expr} " atau " (expr) ").

Penting untuk dicatat bahwa jumlah nama dalam daftar identifier-list harus sesuai dengan jumlah elemen dalam objek yang dihasilkan dari expression .

Ini semua memungkinkan Anda untuk menulis konstruksi formulir:

 const volatile auto && [a,b,c] = Foo{}; 

Dan di sini kita sampai ke tempat "licin" pertama: bertemu ekspresi dari bentuk " auto a = expr; ", Anda biasanya berarti bahwa tipe" a "akan dihitung oleh ekspresi" expr ", dan Anda berharap bahwa dalam ekspresi" const auto& [a,b,c] = expr; "Hal yang sama akan dilakukan, hanya tipe untuk" a,b,c "yang akan menjadi tipe const& elemen" expr "yang sesuai ...

Kebenarannya berbeda: specifier cv-auto ref-operator digunakan untuk menghitung jenis variabel tak terlihat, di mana hasil perhitungan expr ditugaskan (yaitu, kompiler menggantikan " const auto& [a,b,c] = expr " dengan " const auto& e = expr ").

Dengan demikian, entitas tak kasat mata baru muncul (selanjutnya saya akan menyebutnya {e}), namun entitas tersebut sangat berguna: misalnya, ia dapat mematerialisasi objek sementara (karena itu, Anda dapat dengan aman menghubungkannya β€œ const auto& [a,b,c] = Foo {}; ").

Tempat licin kedua mengikuti segera dari penggantian yang dibuat oleh kompiler: jika jenis yang dideduksi untuk {e} bukan referensi, maka hasil expr akan disalin ke {e}.

Jenis apa yang akan dimiliki variabel dalam identifier-list ? Untuk memulainya, ini tidak akan menjadi variabel. Ya, mereka berperilaku seperti variabel nyata dan biasa, tetapi hanya dengan perbedaan yang di dalamnya mereka merujuk ke entitas yang terkait dengannya, dan tipe pernyataan dari variabel "referensi" akan menghasilkan tipe entitas yang dirujuk oleh variabel ini:

 std::tuple<int, float> t(1, 2.f); auto& [a, b] = t; // decltype(a) β€” int, decltype(b) β€” float ++a; // ,  Β« Β»,   t std::cout << std::get<0>(t); //  2 

Jenisnya sendiri didefinisikan sebagai berikut:

  1. Jika {e} adalah sebuah array ( T a[N] ), maka jenisnya akan menjadi satu - T, cv-modifiers akan bertepatan dengan array.
  2. Jika {e} bertipe E dan mendukung antarmuka tuple, strukturnya didefinisikan:

     std::tuple_size<E> 

     std::tuple_element<i, E> 

    dan fungsi:

     get<i>({e}); //  {e}.get<i>() 

    maka tipe setiap variabel akan menjadi tipe std::tuple_element_t<i, E>
  3. Dalam kasus lain, jenis variabel akan sesuai dengan jenis elemen struktur yang mengikat dilakukan.

Jadi, jika sangat singkat, langkah-langkah berikut diambil dengan penautan struktural:

  1. Perhitungan jenis dan inisialisasi entitas tak terlihat {e} berdasarkan jenis pengubah expr dan cv-ref .
  2. Buat pseudo-variabel dan ikat ke elemen {e}.

Secara struktural menghubungkan kelas / struktur Anda


Kendala utama untuk menghubungkan struktur mereka adalah kurangnya refleksi dalam C ++. Bahkan penyusun, yang, kelihatannya, harus tahu pasti bagaimana struktur ini atau itu diatur di dalam, mengalami kesulitan: pengubah akses (publik / swasta / terlindungi) dan pewarisan sangat mempersulit masalah.

Karena kesulitan seperti itu, pembatasan penggunaan kelas mereka sangat ketat (setidaknya untuk saat ini: P1061 , P1096 ):

  1. Semua bidang non-statis internal kelas harus dari kelas dasar yang sama, dan harus tersedia pada saat digunakan.
  2. Atau kelas harus menerapkan "refleksi" (mendukung antarmuka tuple).

 //  «»  struct A { int a; }; struct B : A {}; struct C : A { int c; }; class D { int d; }; auto [a] = A{}; //  (a -> A::a) auto [a] = B{}; //  (a -> B::A::a) auto [a, c] = C{}; // : a  c    auto [d] = D{}; // : d β€” private void D::foo() {    auto [d] = *this; //  (d   ) } 

Implementasi antarmuka tuple memungkinkan Anda untuk menggunakan salah satu kelas Anda untuk mengikat, tetapi terlihat sedikit rumit dan membawa jebakan lain. Mari segera gunakan contoh:

 //  ,      int   class Foo; template<> struct std::tuple_size<Foo> : std::integral_constant<std::size_t, 1> {}; template<> struct std::tuple_element<0, Foo> { using type = int&; }; class Foo { public: template<std::size_t i> std::tuple_element_t<i, Foo> const& get() const; template<std::size_t i> std::tuple_element_t<i, Foo> & get(); private: int _foo = 0; int& _bar = _foo; }; template<> std::tuple_element_t<0, Foo> const& Foo::get<0>() const { return _bar; } template<> std::tuple_element_t<0, Foo> & Foo::get<0>() { return _bar; } 

Sekarang kita ikat:

 Foo foo; const auto& [f1] = foo; const auto [f2] = foo; auto& [f3] = foo; auto [f4] = foo; 

Dan ini saatnya memikirkan jenis apa yang kita dapat? (Siapa pun yang bisa menjawab langsung berhak mendapatkan sweetie yang lezat.)

 decltype(f1); decltype(f2); decltype(f3); decltype(f4); 

Jawaban yang benar
 decltype(f1); // int& decltype(f2); // int& decltype(f3); // int& decltype(f4); // int& ++f1; //     foo._foo,  {e}    const 


Mengapa ini terjadi? Jawabannya terletak pada spesialisasi standar untuk std::tuple_element :

 template<std::size_t i, class T> struct std::tuple_element<i, const T> { using type = std::add_const_t<std::tuple_element_t<i, T>>; }; 

std::add_const tidak menambahkan const ke tipe referensi, jadi tipe untuk Foo akan selalu int& .

Bagaimana cara memenangkan ini? Cukup tambahkan spesialisasi untuk const Foo :

 template<> struct std::tuple_element<0, const Foo> { using type = const int&; }; 

Maka semua jenis akan diharapkan:

 decltype(f1); // const int& decltype(f2); // const int& decltype(f3); // int& decltype(f4); // int& ++f1; //     

Ngomong-ngomong, perilaku yang sama berlaku untuk, misalnya, std::tuple<T&>
- Anda bisa mendapatkan referensi non-konstan ke elemen internal, meskipun objek itu sendiri akan konstan.

Hal yang perlu diingat


  1. β€œ cv-auto ref ” dalam β€œ cv-auto ref [a1..an] = expr ” mengacu pada variabel tak terlihat {e}.
  2. Jika jenis yang disimpulkan {e} tidak direferensikan, {e} akan diinisialisasi dengan menyalin (hati-hati dengan kelas "kelas berat").
  3. Variabel terikat adalah tautan β€œimplisit” (mereka berperilaku seperti tautan, meskipun jenis decltype mengembalikan tipe non-referensi untuk mereka (kecuali variabel tersebut merujuk ke tautan)).
  4. Harus diperhatikan saat menggunakan tipe referensi untuk mengikat.

Return Value Optimization (rvo, salin elision)




Mungkin ini adalah salah satu fitur yang paling banyak dibahas dari standar C ++ 17 (setidaknya di lingkaran teman saya). Dan memang: C ++ 11 membawa semantik gerakan, yang sangat menyederhanakan transfer "internal" objek dan pembuatan berbagai pabrik, dan C ++ 17 secara umum, tampaknya, memungkinkan untuk tidak memikirkan cara mengembalikan objek dari metode pabrik apa pun , - sekarang semuanya harus tanpa menyalin dan secara umum, "segera semuanya akan mekar di Mars" ...

Tetapi mari kita menjadi sedikit realistis: mengoptimalkan nilai kembali bukanlah hal yang paling mudah untuk diterapkan. Saya sangat merekomendasikan menonton presentasi ini dari cppcon2018: Arthur O'Dwyer β€œ Return Value Optimization: Harder Than It Looks ”, di mana penulis menjelaskan mengapa ini sulit.

Spoiler pendek:

Ada yang namanya "slot untuk nilai pengembalian." Slot ini pada dasarnya hanyalah tempat di tumpukan yang dialokasikan oleh orang yang memanggil dan meneruskan ke yang dipanggil. Jika kode yang dipanggil tahu persis objek tunggal mana yang akan dikembalikan, ia dapat langsung dibuat di slot ini secara langsung (asalkan ukuran dan jenis objek dan slotnya sama).

Apa yang mengikuti dari ini? Mari kita bedakan dengan contoh-contoh.

Semuanya akan baik-baik saja di sini - NRVO akan berfungsi, objek akan dibangun segera di "slot":

 Base foo1() { Base a; return a; } 

Di sini tidak lagi mungkin untuk menentukan objek mana yang seharusnya menjadi hasil, sehingga move constructor (c ++ 11) akan secara implisit disebut :

 Base foo2(bool c) { Base a,b; if (c) { return a; } return b; } 

Ini sedikit lebih rumit ... Karena jenis nilai balik berbeda dari tipe yang dideklarasikan, Anda tidak dapat secara implisit memanggil move , sehingga pembuat salinan dipanggil secara default. Untuk mencegah hal ini terjadi, Anda perlu secara eksplisit memanggil move :

 Base foo3(bool c) { Derived a,b; if (c) { return std::move(a); } return std::move(b); } 

Tampaknya ini sama dengan foo2 , tetapi operator ternary adalah hal yang sangat aneh ...

 Base foo4(bool c) { Base a, b; return std::move(c ? a : b); } 

Mirip dengan foo4 , tetapi juga tipe yang berbeda, jadi move diperlukan persis:

 Base foo5(bool c) { Derived a, b; return std::move(c ? a : b); } 

Seperti yang dapat Anda lihat dari contoh-contoh di atas, kita masih harus berpikir tentang bagaimana mengembalikan makna bahkan dalam kasus-kasus yang tampaknya sepele ... Apakah ada cara untuk menyederhanakan hidup Anda sedikit? Ya: dentang untuk beberapa waktu mendukung diagnosis kebutuhan untuk secara eksplisit memanggil move , dan ada beberapa proposal ( P1155 , P0527 ) dalam standar baru yang akan membuat move eksplisit kurang diperlukan.

Hal yang perlu diingat


  1. RVO / NRVO hanya akan berfungsi jika:
    • secara jelas diketahui objek tunggal mana yang harus dibuat dalam "slot nilai balik";
    • mengembalikan objek dan tipe fungsi adalah sama.
  2. Jika ada ambiguitas dalam nilai kembali, maka:
    • jika jenis objek dan fungsi yang dikembalikan cocok, bergerak akan disebut secara implisit;
    • jika tidak, Anda harus secara eksplisit memanggil langkah.
  3. Perhatian dengan operator ternary: itu ringkas, tetapi mungkin memerlukan langkah eksplisit.
  4. Lebih baik menggunakan kompiler dengan diagnostik yang berguna (atau setidaknya analisa statis).

Kesimpulan


Namun saya suka C ++;)

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


All Articles