Mengapa C ++ memerlukan konstruktor default yang disediakan pengguna untuk membuat default objek const?


99

Standar C ++ (bagian 8.5) mengatakan:

Jika sebuah program memanggil inisialisasi default dari sebuah objek tipe T yang memenuhi syarat const, T harus menjadi tipe kelas dengan konstruktor default yang disediakan pengguna.

Mengapa? Saya tidak dapat memikirkan alasan apa pun mengapa konstruktor yang disediakan pengguna diperlukan dalam kasus ini.

struct B{
  B():x(42){}
  int doSomeStuff() const{return x;}
  int x;
};

struct A{
  A(){}//other than "because the standard says so", why is this line required?

  B b;//not required for this example, just to illustrate
      //how this situation isn't totally useless
};

int main(){
  const A a;
}

2
Baris tersebut sepertinya tidak diperlukan dalam contoh Anda (lihat ideone.com/qqiXR ) karena Anda mendeklarasikan tetapi tidak mendefinisikan / menginisialisasi a, tetapi gcc-4.3.4 menerimanya bahkan ketika Anda melakukannya (lihat ideone.com/uHvFS )
Ray Toal

Contoh di atas mendeklarasikan dan mendefinisikan a. Comeau menghasilkan kesalahan "variabel const" a "memerlukan penginisialisasi - kelas" A "tidak secara eksplisit menyatakan konstruktor default" jika baris diberi komentar.
Karu


4
Ini diperbaiki dalam C ++ 11, Anda dapat menulis const A a{}:)
Howard Lovatt

Jawaban:


10

Ini dianggap cacat (terhadap semua versi standar) dan diselesaikan oleh Cacat 253 Kelompok Kerja Inti (CWG) . Kata-kata baru untuk negara standar di http://eel.is/c++draft/dcl.init#7

Jenis kelas T adalah const-default-konstruktif jika inisialisasi default T akan memanggil konstruktor T yang disediakan pengguna (tidak diwariskan dari kelas dasar) atau jika

  • setiap anggota data non-statis non-varian langsung M dari T memiliki penginisialisasi anggota default atau, jika M adalah tipe kelas X (atau lariknya), X adalah konst-default-konstruktif,
  • jika T adalah gabungan dengan setidaknya satu anggota data non-statis, persis satu anggota varian memiliki penginisialisasi anggota default,
  • jika T bukan sebuah serikat, untuk setiap anggota serikat anonim dengan setidaknya satu anggota data non-statis (jika ada), tepat satu anggota data non-statis memiliki penginisialisasi anggota default, dan
  • setiap kelas dasar yang berpotensi dibangun dari T adalah const-default-konstruktif.

Jika sebuah program memanggil inisialisasi-default dari sebuah objek dari tipe T yang memenuhi syarat const, T harus menjadi tipe kelas atau larik yang dapat dibangun konst-default.

Kata-kata ini pada dasarnya berarti bahwa kode yang jelas berfungsi. Jika Anda menginisialisasi semua basis dan anggota Anda, Anda dapat mengatakan A const a;terlepas dari bagaimana atau jika Anda mengeja konstruktor apa pun.

struct A {
};
A const a;

gcc telah menerima ini sejak 4.6.4. dentang telah menerima ini sejak 3.9.0. Visual Studio juga menerima ini (setidaknya pada 2017, tidak yakin apakah lebih cepat).


3
Tapi ini masih melarang struct A { int n; A() = default; }; const A a;sementara mengizinkan struct B { int n; B() {} }; const B b;karena kata-kata baru masih mengatakan "yang disediakan pengguna" bukan "yang dideklarasikan pengguna" dan saya masih bingung mengapa panitia memilih untuk mengecualikan konstruktor default yang secara eksplisit default dari DR ini, memaksa kami untuk membuat kelas kita tidak sepele jika kita ingin objek const dengan anggota yang tidak diinisialisasi.
Oktalis

1
Menarik, tapi masih ada kasus tepi yang pernah saya hadapi. Dengan MyPODmenjadi POD struct, static MyPOD x;- mengandalkan nol-inisialisasi (apakah itu yang benar?) Untuk mengatur variabel anggota dengan tepat - mengkompilasi, tetapi static const MyPOD x;tidak. Apakah ada kemungkinan hal itu akan diperbaiki?
Joshua Green

66

Alasannya adalah jika kelas tersebut tidak memiliki konstruktor yang ditentukan pengguna, maka kelas tersebut dapat berupa POD, dan kelas POD tidak diinisialisasi secara default. Jadi jika Anda mendeklarasikan objek const POD yang tidak diinisialisasi, apa gunanya? Jadi saya pikir Standar memberlakukan aturan ini sehingga objek tersebut benar-benar dapat berguna.

struct POD
{
  int i;
};

POD p1; //uninitialized - but don't worry we can assign some value later on!
p1.i = 10; //assign some value later on!

POD p2 = POD(); //initialized

const POD p3 = POD(); //initialized 

const POD p4; //uninitialized  - error - as we cannot change it later on!

Tetapi jika Anda membuat kelas menjadi non-POD:

struct nonPOD_A
{
    nonPOD_A() {} //this makes non-POD
};

nonPOD_A a1; //initialized 
const nonPOD_A a2; //initialized 

Perhatikan perbedaan antara POD dan non-POD.

Konstruktor yang ditentukan pengguna adalah salah satu cara untuk membuat kelas menjadi non-POD. Ada beberapa cara untuk melakukannya.

struct nonPOD_B
{
    virtual void f() {} //virtual function make it non-POD
};

nonPOD_B b1; //initialized 
const nonPOD_B b2; //initialized 

Perhatikan bahwa nonPOD_B tidak mendefinisikan konstruktor yang ditentukan pengguna. Kompilasi. Ini akan mengkompilasi:

Dan komentari fungsi virtual, maka itu memberikan kesalahan, seperti yang diharapkan:


Nah, saya pikir, Anda salah memahami bagian itu. Ini pertama kali mengatakan ini (§8.5 / 9):

Jika tidak ada penginisialisasi yang ditentukan untuk sebuah objek, dan objek tersebut dari (mungkin memenuhi syarat cv) jenis kelas non-POD (atau lariknya), objek harus diinisialisasi secara default; [...]

Ini berbicara tentang kelas non-POD yang mungkin tipe berkualifikasi cv . Artinya, objek non-POD akan diinisialisasi default jika tidak ada penginisialisasi yang ditentukan. Dan apa yang diinisialisasi default ? Untuk non-POD, spesifikasi mengatakan (§8.5 / 5),

Untuk menginisialisasi default objek tipe T berarti:
- jika T adalah tipe kelas non-POD (klausul 9), konstruktor default untuk T dipanggil (dan inisialisasi tidak berbentuk jika T tidak memiliki konstruktor default yang dapat diakses);

Ini hanya berbicara tentang konstruktor default T, apakah yang ditentukan pengguna atau yang dihasilkan kompiler tidak relevan.

Jika Anda jelas untuk ini, maka pahami apa yang dikatakan spesifikasi selanjutnya ((§8.5 / 9),

[...]; jika objek adalah tipe yang memenuhi syarat const, tipe kelas yang mendasari harus memiliki konstruktor default yang dideklarasikan pengguna.

Jadi teks ini menyiratkan, program akan menjadi tidak benar jika objek adalah tipe POD yang memenuhi syarat const , dan tidak ada penginisialisasi yang ditentukan (karena POD tidak diinisialisasi default):

POD p1; //uninitialized - can be useful - hence allowed
const POD p2; //uninitialized - never useful  - hence not allowed - error

Omong-omong, ini mengkompilasi dengan baik , karena non-POD, dan dapat diinisialisasi secara default .


1
Saya yakin contoh terakhir Anda adalah kesalahan kompilasi - nonPOD_Btidak memiliki konstruktor default yang disediakan pengguna sehingga baris const nonPOD_B b2tersebut tidak diizinkan.
Karu

1
Cara lain untuk membuat kelas menjadi non-POD adalah dengan memberikannya anggota data yang bukan POD (misalnya, struct saya Bdalam pertanyaan). Tetapi konstruktor default yang disediakan pengguna masih diperlukan dalam kasus itu.
Karu

"Jika sebuah program memanggil inisialisasi default dari sebuah objek dari tipe T yang memenuhi syarat konst, T akan menjadi tipe kelas dengan konstruktor default yang disediakan pengguna."
Karu

@Karu: Saya sudah membaca itu. Tampaknya ada bagian lain dalam spesifikasi, yang memungkinkan objek constnon-POD diinisialisasi dengan memanggil default-constructor yang dihasilkan oleh compiler.
Nawaz

2
Tautan ideone Anda sepertinya rusak, dan alangkah baiknya jika jawaban ini dapat diperbarui ke C ++ 11/14 karena §8.5 tidak menyebutkan POD sama sekali.
Oktalis

12

Spekulasi murni di pihak saya, tetapi pertimbangkan bahwa jenis lain juga memiliki batasan serupa:

int main()
{
    const int i; // invalid
}

Jadi tidak hanya aturan ini konsisten, tetapi juga (secara rekursif) mencegah objek unitialized const(sub):

struct X {
    int j;
};
struct A {
    int i;
    X x;
}

int main()
{
    const A a; // a.i and a.x.j in unitialized states!
}

Adapun sisi lain dari pertanyaan (mengizinkannya untuk tipe dengan konstruktor default), saya pikir idenya adalah bahwa tipe dengan konstruktor default yang disediakan pengguna seharusnya selalu dalam keadaan yang masuk akal setelah konstruksi. Perhatikan bahwa aturan seperti yang diizinkan untuk yang berikut:

struct A {
    explicit
    A(int i): initialized(true), i(i) {} // valued constructor

    A(): initialized(false) {}

    bool initialized;
    int i;
};

const A a; // class invariant set up for the object
           // yet we didn't pay the cost of initializing a.i

Kemudian mungkin kita bisa merumuskan aturan seperti 'setidaknya satu anggota harus diinisialisasi dengan bijaksana dalam konstruktor default yang disediakan pengguna', tetapi itu terlalu banyak waktu yang dihabiskan untuk mencoba melindungi dari Murphy. C ++ cenderung mempercayai pemrogram pada poin-poin tertentu.


Tetapi dengan menambahkan A(){}, kesalahan akan hilang, jadi tidak mencegah apa pun. Aturan tidak berfungsi secara rekursif - X(){}tidak pernah diperlukan untuk contoh itu.
Karu

2
Yah, setidaknya dengan memaksa pemrogram untuk menambahkan konstruktor, dia dipaksa untuk memikirkan sebentar masalah tersebut dan mungkin muncul dengan masalah yang tidak sepele
arne

@Karu Saya hanya menjawab setengah pertanyaan - memperbaikinya :)
Luc Danton

4
@arne: Satu-satunya masalah adalah itu programmer yang salah. Orang yang mencoba membuat contoh kelas mungkin memberikan semua pemikiran yang dia inginkan tentang masalah tersebut, tetapi mereka mungkin tidak dapat mengubah kelas. Penulis kelas memikirkan para anggota, melihat bahwa mereka semua diinisialisasi dengan bijaksana oleh konstruktor default implisit, jadi tidak pernah menambahkan satu pun.
Karu

3
Apa yang saya ambil dari bagian standar ini adalah "selalu deklarasikan konstruktor default untuk jenis non-POD, jika seseorang ingin membuat instance const suatu hari nanti". Sepertinya itu berlebihan.
Karu

3

Saya menonton pembicaraan Timur Doumler di Meeting C ++ 2018 dan saya akhirnya menyadari mengapa standar membutuhkan konstruktor yang disediakan pengguna di sini, bukan hanya yang dinyatakan pengguna. Ini berkaitan dengan aturan untuk inisialisasi nilai.

Pertimbangkan dua kelas: Amemiliki konstruktor yang dideklarasikan pengguna , Bmemiliki konstruktor yang disediakan pengguna :

struct A {
    int x;
    A() = default;
};
struct B {
    int x;
    B() {}
};

Sekilas, Anda mungkin mengira kedua konstruktor ini akan berperilaku sama. Tetapi lihat bagaimana inisialisasi nilai berperilaku berbeda, sementara hanya inisialisasi default yang berperilaku sama:

  • A a; adalah inisialisasi default: anggota int x tidak diinisialisasi.
  • B b; adalah inisialisasi default: anggota int x tidak diinisialisasi.
  • A a{};adalah nilai inisialisasi: anggota yang int xadalah nol-diinisialisasi .
  • B b{};adalah inisialisasi nilai: anggota int xbelum diinisialisasi.

Sekarang lihat apa yang terjadi jika kita menambahkan const:

  • const A a;adalah inisialisasi default: ini sakit-terbentuk karena aturan yang dikutip dalam pertanyaan.
  • const B b;adalah inisialisasi default: anggotaint x tidak diinisialisasi.
  • const A a{};adalah nilai inisialisasi: anggota yang int xadalah nol-diinisialisasi .
  • const B b{};adalah inisialisasi nilai: anggota int xbelum diinisialisasi.

constSkalar yang tidak diinisialisasi (misalnya int xanggota) tidak akan berguna: menulis padanya adalah bentuk yang salah (karena itu const) dan membaca darinya adalah UB (karena ia memiliki nilai yang tidak pasti). Jadi aturan ini mencegah Anda dari menciptakan hal seperti itu, dengan memaksa Anda untuk baik menambahkan initialiser atau dengan perilaku berbahaya opt-in dengan menambahkan konstruktor yang disediakan-friendly.

Saya pikir alangkah baiknya memiliki atribut seperti [[uninitialized]]memberi tahu kompiler ketika Anda secara sengaja tidak menginisialisasi objek. Maka kita tidak akan dipaksa untuk membuat kelas kita tidak secara default dapat dibangun untuk mengatasi kasus sudut ini. Atribut ini sebenarnya telah diusulkan , tetapi seperti semua atribut standar lainnya, atribut ini tidak mewajibkan perilaku normatif apa pun, hanya sebagai petunjuk bagi kompiler.


1

Selamat, Anda telah menemukan kasus di mana tidak perlu ada konstruktor yang ditentukan pengguna untuk const deklarasi tanpa penginisialisasi agar masuk akal.

Sekarang, dapatkah Anda membuat pengubahan kata-kata yang masuk akal dari aturan yang mencakup kasus Anda tetapi tetap membuat kasus yang seharusnya ilegal? Apakah kurang dari 5 atau 6 paragraf? Apakah mudah dan jelas bagaimana itu harus diterapkan dalam situasi apa pun?

Saya berpendapat bahwa membuat aturan yang memungkinkan pernyataan yang Anda buat masuk akal sangatlah sulit, dan memastikan bahwa aturan tersebut dapat diterapkan dengan cara yang masuk akal bagi orang-orang saat membaca kode bahkan lebih sulit. Saya lebih memilih aturan yang agak membatasi yang merupakan hal yang benar untuk dilakukan dalam banyak kasus daripada aturan yang sangat bernuansa dan kompleks yang sulit dipahami dan diterapkan.

Pertanyaannya adalah, adakah alasan kuat mengapa aturan tersebut harus lebih kompleks? Adakah beberapa kode yang akan sangat sulit untuk ditulis atau dipahami yang dapat ditulis dengan lebih sederhana jika aturannya lebih kompleks?


1
Berikut saran saya: "Jika sebuah program memanggil inisialisasi default dari sebuah objek dari tipe T yang memenuhi syarat const, T akan menjadi tipe kelas non-POD.". Ini akan membuat const POD x;ilegal seperti const int x;juga ilegal (yang masuk akal, karena ini tidak berguna untuk POD), tetapi membuat const NonPOD x;legal (yang masuk akal, karena itu bisa memiliki subobjek yang berisi konstruktor / destruktor yang berguna, atau memiliki konstruktor / destruktor yang berguna itu sendiri) .
Karu

@Karu - Kata-kata itu mungkin berhasil. Saya sudah terbiasa dengan standar RFC, dan merasa bahwa 'T harus' harus dibaca 'T harus'. Tapi ya, itu bisa berhasil.
Omnifarious

@Karu - Bagaimana dengan struct NonPod {int i; kekosongan maya f () {}}? Tidak masuk akal untuk membuat const NonPod x; hukum.
gruzovator

1
@gruzovator Apakah lebih masuk akal jika Anda memiliki konstruktor default yang dideklarasikan pengguna kosong? Saran saya hanya mencoba untuk menghapus persyaratan standar yang tidak ada gunanya; dengan atau tanpanya masih banyak sekali cara untuk menulis kode yang tidak masuk akal.
Karu

1
@Karu Saya setuju dengan Anda. Karena aturan ini dalam standar, ada banyak kelas yang harus memiliki konstruktor kosong yang ditentukan pengguna . Saya suka perilaku gcc. Ini memungkinkan misalnya struct NonPod { std::string s; }; const NonPod x;Dan memberikan kesalahan ketika NonPod adalahstruct NonPod { int i; std::string s; }; const NonPod x;
gruzovator
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.