Kapan konstruktor privat bukan konstruktor privat?

92

Katakanlah saya memiliki tipe dan saya ingin menjadikan konstruktor defaultnya pribadi. Saya menulis yang berikut ini:

class C {
    C() = default;
};

int main() {
    C c;           // error: C::C() is private within this context (g++)
                   // error: calling a private constructor of class 'C' (clang++)
                   // error C2248: 'C::C' cannot access private member declared in class 'C' (MSVC)
    auto c2 = C(); // error: as above
}

Bagus.

Tapi kemudian, konstruktornya ternyata tidak sepribadi yang saya kira:

class C {
    C() = default;
};

int main() {
    C c{};         // OK on all compilers
    auto c2 = C{}; // OK on all compilers
}    

Ini menurut saya sebagai perilaku yang sangat mengejutkan, tidak terduga, dan secara eksplisit tidak diinginkan. Mengapa ini oke?

Barry
sumber
25
Bukankah C c{};inisialisasi agregat jadi tidak ada konstruktor yang dipanggil?
NathanOliver
5
Apa yang dikatakan @NathanOliver. Anda tidak memiliki konstruktor yang disediakan pengguna, begitu Cjuga dengan agregat.
Kerrek SB
5
@KerrekSB Pada saat yang sama, cukup mengejutkan bagi saya bahwa pengguna yang secara eksplisit mendeklarasikan ctor tidak membuat ctor tersebut disediakan oleh pengguna.
Angew tidak lagi bangga dengan SO
1
@Angew Itu sebabnya kita semua di sini :)
Barry
2
@Angew Jika itu adalah =defaultctor publik , itu akan tampak lebih masuk akal. Tapi private =defaultctor sepertinya merupakan hal penting yang tidak boleh diabaikan. Terlebih lagi, class C { C(); } inline C::C()=default;menjadi sangat berbeda agak mengejutkan.
Yakk - Adam Nevraumont

Jawaban:

61

Triknya ada di C ++ 14 8.4.2 / 5 [dcl.fct.def.default]:

... Sebuah fungsi disediakan oleh pengguna jika ia dideklarasikan oleh pengguna dan tidak secara eksplisit default atau dihapus pada deklarasi pertamanya. ...

Artinya C, konstruktor default sebenarnya tidak disediakan oleh pengguna, karena ini secara eksplisit default pada deklarasi pertamanya. Dengan demikian, Ctidak memiliki konstruktor yang disediakan pengguna dan oleh karena itu merupakan agregat per 8.5.1 / 1 [dcl.init.aggr]:

Sebuah agregat adalah sebuah array atau kelas (Ayat 9) tanpa konstruktor yang disediakan pengguna (12,1), tidak ada anggota data non-statis pribadi atau dilindungi (Ayat 11), tidak ada kelas dasar (Ayat 10), dan tidak ada fungsi virtual (10.3 ).

Angew tidak lagi bangga dengan SO
sumber
13
Akibatnya, cacat standar kecil: fakta bahwa ctor default adalah pribadi diabaikan dalam konteks ini.
Yakk - Adam Nevraumont
2
@Yakk Saya merasa tidak memenuhi syarat untuk menilai itu. Kata-kata tentang ctor yang tidak disediakan oleh pengguna terlihat sangat disengaja.
Angew tidak lagi bangga dengan SO
1
@Yakk: Ya dan tidak. Jika kelas memiliki anggota data, Anda akan memiliki kesempatan untuk menjadikannya pribadi. Tanpa anggota data, hanya ada sedikit situasi di mana situasi ini akan berdampak serius pada siapa pun.
Kerrek SB
2
@KerrekSB Penting jika Anda mencoba menggunakan kelas semacam "token akses", yang mengontrol misalnya siapa yang dapat memanggil fungsi berdasarkan siapa yang dapat membuat objek kelas.
Angew tidak lagi bangga dengan SO
5
@Yakk Bahkan lebih menarik adalah yang C{}bekerja bahkan jika konstruktor deleted.
Barry
56

Anda tidak memanggil konstruktor default, Anda menggunakan inisialisasi agregat pada jenis agregat. Jenis agregat diizinkan untuk memiliki konstruktor default, selama itu default di tempat pertama kali dinyatakan:

Dari [dcl.init.aggr] / 1 :

Agregat adalah larik atau kelas (Klausul [kelas]) dengan

  • tidak ada konstruktor yang disediakan pengguna ([class.ctor]) (termasuk yang diwariskan ([namespace.udecl]) dari kelas dasar),
  • tidak ada anggota data non-statis pribadi atau dilindungi (Klausul [class.access]),
  • tidak ada fungsi virtual ([class.virtual]), dan
  • tidak ada kelas dasar virtual, privat, atau terlindungi ([class.mi]).

dan dari [dcl.fct.def.default] / 5

Fungsi yang dideklarasikan secara eksplisit dan fungsi yang dideklarasikan secara implisit secara kolektif disebut fungsi default, dan implementasi akan memberikan definisi implisit untuk fungsi tersebut ([class.ctor] [class.dtor], [class.copy]), yang mungkin berarti mendefinisikannya sebagai dihapus . [Catatan: Mendeklarasikan suatu fungsi sebagai default setelah deklarasi pertamanya dapat memberikan eksekusi yang efisien dan definisi yang ringkas sambil mengaktifkan antarmuka biner yang stabil ke basis kode yang berkembang. - catatan akhir]Sebuah fungsi disediakan oleh pengguna jika ia dideklarasikan oleh pengguna dan tidak secara eksplisit didefaultkan atau dihapus pada deklarasi pertamanya. Fungsi default-eksplisit yang disediakan oleh pengguna (yaitu, secara eksplisit default setelah deklarasi pertamanya) ditentukan pada titik di mana ia secara eksplisit default; jika fungsi seperti itu secara implisit didefinisikan sebagai dihapus, program tersebut memiliki format yang salah.

Jadi, persyaratan kami untuk agregat adalah:

  • tidak ada anggota non-publik
  • tidak ada fungsi virtual
  • tidak ada kelas dasar virtual atau non-publik
  • tidak ada konstruktor yang disediakan pengguna yang diwarisi atau sebaliknya, yang hanya mengizinkan konstruktor yang:
    • dideklarasikan secara implisit, atau
    • secara eksplisit dideklarasikan dan didefinisikan sebagai default pada saat yang sama.

C memenuhi semua persyaratan ini.

Biasanya, Anda dapat menyingkirkan perilaku konstruksi default palsu ini hanya dengan menyediakan konstruktor default kosong, atau dengan mendefinisikan konstruktor sebagai default setelah mendeklarasikannya:

class C {
    C(){}
};
// --or--
class C {
    C();
};
inline C::C() = default;
jaggedSpire
sumber
2
Saya suka jawaban ini agak lebih baik daripada jawaban Angew, tapi menurut saya ini akan mendapat manfaat dari ringkasan di awal paling banyak dalam dua kalimat.
PJTraill
7

Angew ini dan jaggedSpire ini jawaban yang sangat baik dan berlaku untuk. Dan. Dan.

Namun, dalam , banyak hal berubah sedikit dan contoh di OP tidak akan lagi dikompilasi:

class C {
    C() = default;
};

C p;          // always error
auto q = C(); // always error
C r{};        // ok on C++11 thru C++17, error on C++20
auto s = C{}; // ok on C++11 thru C++17, error on C++20

Seperti yang ditunjukkan oleh dua jawaban, alasan kedua deklarasi terakhir berfungsi adalah karena Cmerupakan agregat dan ini adalah inisialisasi agregat. Namun, sebagai hasil dari P1008 (menggunakan contoh motivasi yang tidak terlalu berbeda dari OP), definisi agregat berubah dalam C ++ 20 menjadi, dari [dcl.init.aggr] / 1 :

Agregat adalah larik atau kelas ([class]) dengan

  • tidak ada yang diumumkan pengguna konstruktor yang atau diwariskan pengguna ([class.ctor]),
  • tidak ada anggota data non-statis langsung pribadi atau dilindungi ([class.access]),
  • tidak ada fungsi virtual ([class.virtual]), dan
  • tidak ada kelas dasar virtual, privat, atau dilindungi ([class.mi]).

Penekanan milikku. Sekarang persyaratannya adalah tidak ada konstruktor yang dideklarasikan pengguna , sedangkan dulu (karena kedua pengguna mengutip jawaban mereka dan dapat dilihat secara historis untuk C ++ 11 , C ++ 14 , dan C ++ 17 ) tidak ada konstruktor yang disediakan pengguna . Konstruktor default untuk Cdideklarasikan oleh pengguna, tetapi tidak disediakan pengguna, dan karenanya berhenti menjadi agregat di C ++ 20.


Berikut adalah contoh ilustrasi lain dari perubahan agregat:

class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};

Bbukan agregat di C ++ 11 atau C ++ 14 karena memiliki kelas dasar. Akibatnya, B{}cukup panggil konstruktor default (dideklarasikan pengguna tetapi tidak disediakan pengguna), yang memiliki akses ke Akonstruktor default yang dilindungi.

Dalam C ++ 17, sebagai hasil dari P0017 , agregat diperluas untuk memungkinkan kelas dasar. Badalah agregat dalam C ++ 17, yang berarti B{}inisialisasi agregat yang harus menginisialisasi semua subobjek - termasuk Asubobjek. Tetapi karena Akonstruktor default dilindungi, kami tidak memiliki akses ke sana, jadi inisialisasi ini salah bentuk.

Dalam C ++ 20, karena Bkonstruktor yang dideklarasikan oleh pengguna, ia kembali berhenti menjadi agregat, jadi B{}kembali ke pemanggilan konstruktor default dan ini adalah inisialisasi yang dibentuk dengan baik.

Barry
sumber