Sintaks baru “= default” di C ++ 11

136

Saya tidak mengerti mengapa saya melakukan ini:

struct S { 
    int a; 
    S(int aa) : a(aa) {} 
    S() = default; 
};

Kenapa tidak bilang saja:

S() {} // instead of S() = default;

mengapa membawa sintaksis baru untuk itu?

pengguna3111311
sumber
30
Nitpick: defaultbukan kata kunci baru, itu hanya penggunaan baru kata kunci yang sudah dipesan.
Mey be Pertanyaan ini mungkin bisa membantu Anda.
FreeNickname
7
Selain jawaban lain, saya juga berpendapat bahwa '= default;' lebih banyak mendokumentasikan diri.
Tandai
Terkait: stackoverflow.com/questions/13576055/…
Gabriel Staples

Jawaban:

136

Konstruktor default default secara spesifik didefinisikan sebagai sama dengan konstruktor default yang ditentukan pengguna tanpa daftar inisialisasi dan pernyataan gabungan kosong.

§12.1 / 6 [class.ctor] Konstruktor default yang default dan tidak didefinisikan sebagai dihapus didefinisikan secara implisit ketika digunakan untuk membuat objek tipe kelasnya atau ketika secara eksplisit default setelah deklarasi pertama. Konstruktor default yang didefinisikan secara implisit melakukan serangkaian inisialisasi kelas yang akan dilakukan oleh konstruktor default yang ditulis pengguna untuk kelas tersebut tanpa ctor-initializer (12.6.2) dan pernyataan gabungan kosong. [...]

Namun, sementara kedua konstruktor akan berperilaku sama, memberikan implementasi kosong tidak memengaruhi beberapa properti kelas. Memberikan konstruktor yang ditentukan pengguna, meskipun tidak menghasilkan apa-apa, membuat tipe tersebut tidak agregat dan juga tidak sepele . Jika Anda ingin kelas Anda menjadi tipe agregat atau trivial (atau berdasarkan transitivitas, tipe POD), maka Anda perlu menggunakannya = default.

§8.5.1 / 1 [dcl.init.aggr] Agregat adalah array atau kelas tanpa konstruktor yang disediakan pengguna, [dan ...]

§12.1 / 5 [class.ctor] Konstruktor default adalah sepele jika tidak disediakan oleh pengguna dan [...]

§9 / 6 [kelas] Kelas sepele adalah kelas yang memiliki konstruktor default sepele dan [...]

Untuk menunjukkan:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() { };
};

int main() {
    static_assert(std::is_trivial<X>::value, "X should be trivial");
    static_assert(std::is_pod<X>::value, "X should be POD");
    
    static_assert(!std::is_trivial<Y>::value, "Y should not be trivial");
    static_assert(!std::is_pod<Y>::value, "Y should not be POD");
}

Selain itu, secara eksplisit default konstruktor akan membuatnya constexprjika konstruktor implisit akan dan juga akan memberikan spesifikasi pengecualian yang sama dengan konstruktor implisit. Dalam kasus yang Anda berikan, konstruktor implisit tidak akan constexpr(karena akan membuat anggota data tidak diinisialisasi) dan juga akan memiliki spesifikasi pengecualian kosong, jadi tidak ada perbedaan. Tapi ya, dalam kasus umum Anda bisa secara manual menentukan constexprdan spesifikasi pengecualian untuk mencocokkan konstruktor implisit.

Menggunakan = defaultmemang membawa keseragaman, karena juga dapat digunakan dengan menyalin / memindahkan konstruktor dan destruktor. Konstruktor salinan kosong, misalnya, tidak akan melakukan hal yang sama dengan konstruktor salin default (yang akan melakukan salinan anggota-anggotanya). Menggunakan sintaks = default(atau = delete) secara seragam untuk masing-masing fungsi anggota khusus ini membuat kode Anda lebih mudah dibaca dengan secara eksplisit menyatakan maksud Anda.

Joseph Mansfield
sumber
Hampir. 12.1 / 6: "Jika konstruktor default yang ditulis pengguna akan memenuhi persyaratan constexprkonstruktor (7.1.5), konstruktor default yang didefinisikan secara implisit adalah constexpr."
Casey
Sebenarnya, 8.4.2 / 2 lebih informatif: "Jika suatu fungsi secara eksplisit gagal pada deklarasi pertamanya, (a) secara implisit dianggap constexprjika deklarasi implisit akan, (b) secara implisit dianggap memiliki fungsi yang sama. pengecualian-spesifikasi seolah-olah telah dinyatakan secara implisit (15,4), ... "Tidak ada bedanya dalam kasus khusus ini, tetapi secara umum foo() = default;memiliki sedikit kelebihan foo() {}.
Casey
2
Anda mengatakan tidak ada perbedaan, dan kemudian menjelaskan perbedaannya?
@ Hvd Dalam hal ini tidak ada perbedaan, karena deklarasi implisit tidak akan constexpr(karena anggota data dibiarkan tidak diinisialisasi) dan spesifikasi pengecualiannya memungkinkan semua pengecualian. Saya akan membuatnya lebih jelas.
Joseph Mansfield
2
Terimakasih atas klarifikasinya. Tampaknya masih ada perbedaan, dengan constexpr(yang Anda sebutkan seharusnya tidak membuat perbedaan di sini): struct S1 { int m; S1() {} S1(int m) : m(m) {} }; struct S2 { int m; S2() = default; S2(int m) : m(m) {} }; constexpr S1 s1 {}; constexpr S2 s2 {};Hanya s1memberikan kesalahan, bukan s2. Baik dentang dan g ++.
10

Saya punya contoh yang akan menunjukkan perbedaan:

#include <iostream>

using namespace std;
class A 
{
public:
    int x;
    A(){}
};

class B 
{
public:
    int x;
    B()=default;
};


int main() 
{ 
    int x = 5;
    new(&x)A(); // Call for empty constructor, which does nothing
    cout << x << endl;
    new(&x)B; // Call for default constructor
    cout << x << endl;
    new(&x)B(); // Call for default constructor + Value initialization
    cout << x << endl;
    return 0; 
} 

Keluaran:

5
5
0

Seperti yang kita lihat panggilan untuk konstruktor A () kosong tidak menginisialisasi anggota, sementara B () melakukannya.

Slavenskij
sumber
7
tolong jelaskan sintaks ini -> baru (& x) A ();
Vencat
5
Kami membuat objek baru di memori mulai dari alamat variabel x (bukan alokasi memori baru). Sintaks ini digunakan untuk membuat objek pada memori yang dialokasikan sebelumnya. Seperti dalam kasus kami ukuran B = ukuran int, jadi baru (& x) A () akan membuat objek baru di tempat variabel x.
Slavenskij
Terima kasih atas penjelasan anda
Vencat
1
Saya mendapatkan hasil yang berbeda dengan gcc 8.3: ideone.com/XouXux
Adam.Er8
Bahkan dengan C ++ 14, saya mendapatkan hasil yang berbeda: ideone.com/CQphuT
Mayank Bhushan
9

n2210 memberikan beberapa alasan:

Manajemen default memiliki beberapa masalah:

  • Definisi konstruktor digabungkan; mendeklarasikan konstruktor apa pun akan menekan konstruktor default.
  • Default destructor tidak sesuai untuk kelas polimorfik, membutuhkan definisi eksplisit.
  • Setelah default ditekan, tidak ada cara untuk menghidupkannya kembali.
  • Implementasi default seringkali lebih efisien daripada implementasi yang ditentukan secara manual.
  • Implementasi non-default adalah non-sepele, yang mempengaruhi semantik tipe, misalnya membuat tipe non-POD.
  • Tidak ada cara untuk melarang fungsi anggota khusus atau operator global tanpa menyatakan pengganti (non-sepele).

type::type() = default;
type::type() { x = 3; }

Dalam beberapa kasus, badan kelas dapat berubah tanpa memerlukan perubahan dalam definisi fungsi anggota karena perubahan standar dengan deklarasi anggota tambahan.

Lihat Rule-of-Three menjadi Rule-of-Five dengan C ++ 11? :

Perhatikan bahwa memindahkan konstruktor dan memindahkan operator penugasan tidak akan dihasilkan untuk kelas yang secara eksplisit menyatakan salah satu fungsi anggota khusus lainnya, yang menyalin konstruktor dan menyalin operator tidak akan dihasilkan untuk kelas yang secara eksplisit menyatakan memindahkan konstruktor atau memindahkan operator penugasan, dan bahwa kelas dengan destruktor yang dinyatakan secara eksplisit dan konstruktor salinan yang ditentukan secara implisit atau operator penugasan salinan yang didefinisikan secara implisit dianggap usang

Komunitas
sumber
1
Mereka adalah alasan untuk memiliki = defaultsecara umum, bukan alasan untuk melakukan = defaultkonstruktor vs melakukan { }.
Joseph Mansfield
@JosephMansfield Benar, tapi karena {}sudah menjadi ciri bahasa sebelum pengenalan =default, alasan-alasan ini tidak secara implisit bergantung pada perbedaan (misalnya "tidak ada cara untuk membangkitkan [default ditekan]" menyiratkan bahwa {}adalah tidak setara dengan default ).
Kyle Strand
7

Ini masalah semantik dalam beberapa kasus. Tidak terlalu jelas dengan konstruktor default, tetapi menjadi jelas dengan fungsi anggota yang dihasilkan kompiler lainnya.

Untuk konstruktor default, dimungkinkan untuk membuat konstruktor default apa pun dengan badan kosong dianggap sebagai kandidat untuk menjadi konstruktor sepele, sama seperti menggunakan =default. Bagaimanapun, konstruktor default kosong yang lama adalah legal C ++ .

struct S { 
  int a; 
  S() {} // legal C++ 
};

Apakah kompiler memahami konstruktor ini sepele atau tidak relevan dalam banyak kasus di luar optimasi (manual atau kompiler).

Namun, upaya ini untuk memperlakukan badan fungsi kosong sebagai "default" rusak sepenuhnya untuk jenis fungsi anggota lainnya. Pertimbangkan pembuat salinan:

struct S { 
  int a; 
  S() {}
  S(const S&) {} // legal, but semantically wrong
};

Dalam kasus di atas, pembuat salinan yang ditulis dengan badan kosong sekarang salah . Sebenarnya tidak lagi menyalin apa pun. Ini adalah serangkaian semantik yang sangat berbeda dari semantik konstruktor salin standar. Perilaku yang diinginkan mengharuskan Anda untuk menulis beberapa kode:

struct S { 
  int a; 
  S() {}
  S(const S& src) : a(src.a) {} // fixed
};

Meskipun dengan kasus sederhana ini, bagaimanapun, menjadi lebih banyak beban bagi kompiler untuk memverifikasi bahwa copy constructor identik dengan yang akan dihasilkan sendiri atau untuk itu untuk melihat bahwa copy constructor itu sepele (setara dengan memcpy, pada dasarnya ). Kompiler harus memeriksa setiap ekspresi penginisialisasi anggota dan memastikannya identik dengan ekspresi untuk mengakses anggota yang sesuai sumber dan tidak ada yang lain, pastikan tidak ada anggota yang tersisa dengan konstruksi default non-sepele, dll. Ini mundur dengan cara proses kompiler akan gunakan untuk memverifikasi bahwa versi yang dihasilkan sendiri dari fungsi ini sepele.

Pertimbangkan operator penugasan salinan yang bisa lebih adil, terutama dalam kasus non-sepele. Ini adalah satu ton boiler-plate yang Anda tidak ingin harus menulis untuk banyak kelas tetapi Anda tetap harus melakukannya di C ++ 03:

struct T { 
  std::shared_ptr<int> b; 
  T(); // the usual definitions
  T(const T&);
  T& operator=(const T& src) {
    if (this != &src) // not actually needed for this simple example
      b = src.b; // non-trivial operation
    return *this;
};

Itu adalah kasus sederhana, tetapi sudah lebih dari kode yang Anda ingin dipaksa untuk menulis untuk tipe sederhana seperti itu T(terutama sekali kita melemparkan operasi ke dalam campuran). Kita tidak dapat mengandalkan tubuh kosong yang berarti "isi default" karena tubuh kosong sudah benar-benar valid dan memiliki makna yang jelas. Bahkan, jika badan kosong digunakan untuk menunjukkan "isi default" maka tidak akan ada cara untuk secara eksplisit membuat konstruktor no-op copy atau sejenisnya.

Lagi-lagi masalah konsistensi. Tubuh kosong berarti "tidak melakukan apa-apa" tetapi untuk hal-hal seperti copy constructor Anda benar-benar tidak ingin "tidak melakukan apa-apa" melainkan "melakukan semua hal yang biasanya Anda lakukan jika tidak ditekan." Oleh karena itu =default. Ini diperlukan untuk mengatasi fungsi anggota yang dihasilkan kompiler yang ditekan seperti menyalin / memindahkan konstruktor dan operator penugasan. Maka hanya "jelas" untuk membuatnya bekerja untuk konstruktor default juga.

Mungkin menyenangkan untuk membuat konstruktor default dengan benda kosong dan konstruktor anggota / basis yang sepele juga dianggap sepele seperti jika mereka ingin =defaultmembuat kode lama menjadi lebih optimal dalam beberapa kasus, tetapi sebagian besar kode tingkat rendah bergantung pada hal sepele konstruktor default untuk optimisasi juga bergantung pada konstruktor salinan sepele. Jika Anda harus pergi dan "memperbaiki" semua konstruktor salinan lama Anda, itu benar-benar tidak banyak keharusan untuk memperbaiki semua konstruktor default lama Anda. Ini juga jauh lebih jelas dan lebih jelas menggunakan eksplisit =defaultuntuk menunjukkan niat Anda.

Ada beberapa hal lain yang akan dilakukan oleh fungsi anggota yang dikompilasi oleh kompiler yaitu Anda harus secara eksplisit membuat perubahan untuk mendukung. Mendukung constexprkonstruktor default adalah salah satu contohnya. Hanya lebih mudah digunakan secara mental =defaultdaripada harus menandai fungsi dengan semua kata kunci khusus lainnya dan yang tersirat oleh =defaultdan itu adalah salah satu tema C ++ 11: membuat bahasa lebih mudah. Masih ada banyak kutil dan kompromi back-compat, tetapi jelas bahwa ini adalah langkah besar ke depan dari C ++ 03 ketika datang untuk kemudahan penggunaan.

Sean Middleditch
sumber
Saya punya masalah di mana saya harapkan = defaultakan terjadi a=0;dan tidak! Saya harus menjatuhkannya demi : a(0). Saya masih bingung tentang seberapa bermanfaatkah = defaultini, apakah ini tentang kinerja? apakah itu akan pecah di suatu tempat jika saya tidak menggunakannya = default? Saya mencoba membaca semua jawaban di sini, membeli, saya baru mengenal beberapa hal c ++ dan saya mengalami banyak kesulitan memahaminya.
Aquarius Power
@AquariusPower: ini bukan "hanya" tentang kinerja tetapi juga diperlukan dalam beberapa kasus seputar pengecualian dan semantik lainnya. Yaitu, operator default bisa sepele tetapi operator non-default tidak pernah sepele, dan beberapa kode akan menggunakan teknik meta-pemrograman untuk mengubah perilaku atau bahkan melarang jenis dengan operasi non-sepele. a=0Contoh Anda adalah karena perilaku tipe sepele, yang merupakan topik yang terpisah (meskipun terkait).
Sean Middleditch
apakah itu berarti mungkin untuk memiliki = defaultdan masih aakan memberikan =0? dalam beberapa cara? Anda pikir saya bisa membuat pertanyaan baru seperti "bagaimana memiliki konstruktor = defaultdan memberikan bidang akan diinisialisasi dengan benar?", btw saya punya masalah dalam structdan tidak class, dan aplikasi berjalan dengan benar bahkan tidak menggunakan = default, saya bisa tambahkan minimum struct pada pertanyaan itu jika itu bagus :)
Aquarius Power
1
@AquariusPower: Anda bisa menggunakan inisialisasi anggota data non-statis. Tulis struct Anda seperti ini: struct { int a = 0; };Jika Anda kemudian memutuskan Anda membutuhkan konstruktor, Anda dapat mengaturnya secara default, tetapi perhatikan bahwa jenisnya tidak akan sepele (yang tidak masalah).
Sean Middleditch
2

Karena penghentian std::is_poddan alternatifnya std::is_trivial && std::is_standard_layout, cuplikan dari jawaban @JosephMansfield menjadi:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() {}
};

int main() {
    static_assert(std::is_trivial_v<X>, "X should be trivial");
    static_assert(std::is_standard_layout_v<X>, "X should be standard layout");

    static_assert(!std::is_trivial_v<Y>, "Y should not be trivial");
    static_assert(std::is_standard_layout_v<Y>, "Y should be standard layout");
}

Perhatikan bahwa Ytata letak masih standar.

AnqurVanillapy
sumber