C ++ zero inisialisasi - Mengapa `b` dalam program ini tidak diinisialisasi, tetapi ʻa` diinisialisasi?

136

Menurut jawaban yang diterima (dan hanya) untuk pertanyaan Stack Overflow ini ,

Mendefinisikan konstruktor dengan

MyTest() = default;

sebagai gantinya akan menginisialisasi objek dengan nol.

Lalu mengapa berikut ini,

#include <iostream>

struct foo {
    foo() = default;
    int a;
};

struct bar {
    bar();
    int b;
};

bar::bar() = default;

int main() {
    foo a{};
    bar b{};
    std::cout << a.a << ' ' << b.b;
}

menghasilkan keluaran ini:

0 32766

Kedua konstruktor didefinisikan sebagai default? Baik? Dan untuk jenis POD, inisialisasi default adalah inisialisasi nol.

Dan menurut jawaban yang diterima untuk pertanyaan ini ,

  1. Jika anggota POD tidak diinisialisasi dalam konstruktor atau melalui inisialisasi dalam kelas C ++ 11, ia diinisialisasi secara default.

  2. Jawabannya sama terlepas dari tumpukan atau heap.

  3. Di C ++ 98 (dan bukan setelahnya), new int () ditetapkan sebagai melakukan inisialisasi nol.

Meskipun mencoba untuk membungkus kepala saya (meskipun kecil ) di sekitar konstruktor default dan inisialisasi default , saya tidak dapat memberikan penjelasan.

Dodgers bebek
sumber
3
Menariknya, saya bahkan mendapat peringatan untuk b: main.cpp: 18: 34: peringatan: 'b.bar::b' digunakan tanpa inisialisasi dalam fungsi ini [-Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
tkausl
8
barkonstruktor adalah pengguna yang disediakan sedangkan fookonstruktor adalah yang default.
Jarod42
2
@ PeteBecker, saya mengerti itu. Bagaimana saya bisa mengguncang RAM saya sedikit sehingga jika tidak ada nol di sana, sekarang harus menjadi sesuatu yang lain. ;) ps Saya menjalankan program belasan kali. Ini bukan program besar. Anda dapat menjalankannya dan mengujinya di sistem Anda. aadalah nol. btidak. Sepertinya adiinisialisasi.
Duck Dodgers
2
@JoeyMallone Mengenai "bagaimana ini disediakan pengguna": Tidak ada jaminan bahwa definisi bar::bar()terlihat di main()- definisi ini dapat didefinisikan dalam unit kompilasi terpisah dan melakukan sesuatu yang sangat tidak sepele sementara main()hanya deklarasi yang terlihat. Saya pikir Anda akan setuju bahwa perilaku ini tidak boleh berubah tergantung pada apakah Anda menempatkan bar::bar()definisi di unit kompilasi terpisah atau tidak (bahkan jika seluruh situasi tidak intuitif).
Max Langhof
2
@balki Atau int a = 0;apakah Anda ingin benar-benar eksplisit.
NathanOliver

Jawaban:

110

Masalahnya di sini cukup halus. Anda akan berpikir begitu

bar::bar() = default;

akan memberi Anda compiler yang dihasilkan konstruktor default, dan memang demikian, tetapi sekarang dianggap disediakan oleh pengguna. [dcl.fct.def.default] / 5 status:

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.ctor], [class.copy.assign ]), yang mungkin berarti mendefinisikannya sebagai dihapus. 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 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 tidak berbentuk. [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]

penekanan milikku

Jadi kita dapat melihat bahwa karena Anda tidak melakukan default bar()saat pertama kali mendeklarasikannya, sekarang dianggap disediakan oleh pengguna. Karena itu [dcl.init] /8.2

jika T adalah tipe kelas (mungkin memenuhi syarat cv) tanpa konstruktor default yang disediakan pengguna atau dihapus, maka objek tersebut diinisialisasi nol dan batasan semantik untuk inisialisasi default dicentang, dan jika T memiliki konstruktor default non-sepele , objek tersebut diinisialisasi secara default;

tidak lagi berlaku dan kami tidak menginisialisasi nilai btetapi sebagai gantinya secara default menginisialisasi per [dcl.init] /8.1

jika T adalah tipe kelas (mungkin berkualifikasi cv) ([class]) tanpa konstruktor default ([class.default.ctor]) atau konstruktor default yang disediakan pengguna atau dihapus, maka objek tersebut diinisialisasi secara default ;

NathanOliver
sumber
52
Maksud saya (*_*).... Jika untuk menggunakan konstruksi dasar bahasa, saya perlu membaca cetakan kecil draf bahasa, maka Haleluya! Tapi mungkin itu yang Anda katakan.
Duck Dodgers
12
@balki Ya, melakukan bar::bar() = defaultout of line sama dengan melakukan bar::bar(){}inline.
NathanOliver
15
@JoeyMallone Ya, C ++ bisa sangat rumit. Saya tidak yakin apa alasannya.
NathanOliver
3
Jika ada deklarasi sebelumnya, maka definisi berikutnya dengan kata kunci default TIDAK akan menginisialisasi anggota. Baik? Ini benar. Itulah yang terjadi di sini.
NathanOliver
6
Alasannya ada di kutipan Anda: inti dari default out-of-line adalah untuk "memberikan eksekusi yang efisien dan definisi yang ringkas sambil mengaktifkan antarmuka biner yang stabil ke basis kode yang berkembang", dengan kata lain, memungkinkan Anda untuk beralih ke tubuh yang ditulis pengguna nanti jika perlu tanpa melanggar ABI. Perhatikan bahwa definisi out-of-line tidak secara implisit sebaris sehingga hanya dapat muncul dalam satu TU secara default; TU lain yang melihat definisi kelas saja tidak memiliki cara untuk mengetahui apakah itu secara eksplisit didefinisikan sebagai default.
TC
25

Perbedaan perilaku berasal dari fakta bahwa, menurut [dcl.fct.def.default]/5, bar::bardisediakan pengguna di mana foo::foobukan 1 . Akibatnya, foo::fooakan nilai-menginisialisasi anggotanya (artinya: nol-menginisialisasi foo::a ) tetapi bar::barakan tetap tidak diinisialisasi 2 .


1) [dcl.fct.def.default]/5

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

2)

Dari [dcl.init # 6] :

Untuk menginisialisasi nilai objek tipe T berarti:

  • jika T adalah tipe kelas (mungkin berkualifikasi cv) tanpa konstruktor default ([class.ctor]) atau konstruktor default yang disediakan pengguna atau dihapus, maka objek tersebut diinisialisasi secara default;

  • jika T adalah tipe kelas (mungkin memenuhi syarat cv) tanpa konstruktor default yang disediakan pengguna atau dihapus, maka objek tersebut diinisialisasi nol dan batasan semantik untuk inisialisasi default dicentang, dan jika T memiliki konstruktor default non-sepele , objek tersebut diinisialisasi secara default;

  • ...

Dari [dcl.init.list] :

Daftar-inisialisasi suatu objek atau referensi tipe T didefinisikan sebagai berikut:

  • ...

  • Sebaliknya, jika daftar penginisialisasi tidak memiliki elemen dan T adalah tipe kelas dengan konstruktor default, objek tersebut diinisialisasi nilai.

Dari jawaban Vittorio Romeo

YSC
sumber
10

Dari cppreference :

Inisialisasi agregat menginisialisasi agregat. Ini adalah bentuk inisialisasi daftar.

Agregat adalah salah satu dari jenis berikut:

[menggunting]

  • tipe kelas [snip], yang memiliki

    • [snip] (ada variasi untuk berbagai versi standar)

    • tidak ada konstruktor yang disediakan, diwariskan, atau eksplisit oleh pengguna (konstruktor yang secara eksplisit default atau dihapus diizinkan)

    • [snip] (ada lebih banyak aturan, yang berlaku untuk kedua kelas)

Dengan definisi ini, fooadalah agregat, sedangkan barbukan (memiliki konstruktor non-default yang disediakan pengguna).

Oleh karena itu foo, T object {arg1, arg2, ...};adalah sintaks untuk inisialisasi agregat.

Efek dari inisialisasi agregat adalah:

  • [snip] (beberapa detail tidak relevan dengan kasus ini)

  • Jika jumlah klausa penginisialisasi kurang dari jumlah anggota atau daftar penginisialisasi benar-benar kosong, anggota yang tersisa akan diinisialisasi nilai .

Oleh karena itu a.aadalah nilai yang diinisialisasi, yang intberarti inisialisasi nol.

Sebab bar, T object {};di sisi lain adalah inisialisasi nilai (dari instance kelas, bukan inisialisasi nilai anggota!). Karena ini adalah tipe kelas dengan konstruktor default, konstruktor default dipanggil. Konstruktor default yang Anda tetapkan default menginisialisasi anggota (karena tidak memiliki inisialisasi anggota), yang dalam kasus int(dengan penyimpanan non-statis) meninggalkan b.bdengan nilai yang tidak pasti.

Dan untuk tipe pod, inisialisasi defaultnya adalah inisialisasi nol.

Tidak. Ini salah.


NB Sepatah kata tentang percobaan Anda dan kesimpulan Anda: Melihat bahwa keluaran adalah nol tidak selalu berarti bahwa variabel diinisialisasi nol. Nol adalah angka yang sangat mungkin untuk nilai sampah.

untuk itu saya menjalankan program mungkin 5 ~ 6 kali sebelum posting dan sekitar 10 kali sekarang, a selalu nol. b berubah sedikit.

Fakta bahwa nilainya sama beberapa kali tidak selalu berarti bahwa nilainya juga telah dimulai.

Saya juga mencoba dengan set (CMAKE_CXX_STANDARD 14). Hasilnya sama saja.

Fakta bahwa hasilnya sama dengan beberapa opsi kompilator tidak berarti variabel tersebut diinisialisasi. (Meskipun dalam beberapa kasus, mengubah versi standar dapat mengubah apakah itu sudah dimulai).

Bagaimana saya bisa mengguncang RAM saya sedikit sehingga jika tidak ada nol di sana, sekarang harus menjadi sesuatu yang lain

Tidak ada cara yang dijamin dalam C ++ untuk membuat nilai nilai yang tidak diinisialisasi tampak bukan nol.

Satu-satunya cara untuk mengetahui bahwa variabel diinisialisasi adalah dengan membandingkan program dengan aturan bahasa dan memverifikasi bahwa aturan tersebut menyatakan bahwa ia telah diinisialisasi. Dalam hal a.aini memang dimulai.

eerorika
sumber
"Konstruktor default yang Anda tetapkan secara default menginisialisasi anggota (karena tidak memiliki inisialisasi anggota), yang jika int meninggalkannya dengan nilai yang tidak pasti." -> eh! "untuk jenis pod, inisialisasi defaultnya adalah inisialisasi nol." atau apakah saya salah?
Duck Dodgers
2
@JoeyMallone Inisialisasi default jenis POD adalah tidak ada inisialisasi.
NathanOliver
@NathanOliver, Lalu saya bahkan lebih bingung. Lalu kok bisa adiinisialisasi. Saya berpikir aadalah inisialisasi default dan inisialisasi default untuk POD anggota adalah, inisialisasi nol. Apakah akemudian hanya untungnya selalu datang nol, tidak peduli berapa kali saya menjalankan program ini.
Duck Dodgers
@JoeyMallone Then how come a is initialized.Karena ini adalah nilai yang diinisialisasi. I was thinking a is default initializedBukan itu.
eerorika
3
@JoeyMallone Jangan khawatir tentang itu. Anda bisa membuat buku dari inisialisasi di C ++. Jika Anda mendapat kesempatan, CppCon di youtube memiliki beberapa video tentang inisialisasi dengan yang paling mengecewakan (seperti menunjukkan betapa buruknya) adalah youtube.com/watch?v=7DTlWPgX6zs
NathanOliver
0

Yah, saya mencoba menjalankan cuplikan yang Anda berikan sebagai test.cpp, melalui gcc & clang dan beberapa tingkat pengoptimalan:

steve@steve-pc /tmp> g++ -o test.gcc.O0 test.cpp
                                                                              [ 0s828 | Jan 27 01:16PM ]
steve@steve-pc /tmp> g++ -o test.gcc.O2 -O2 test.cpp
                                                                              [ 0s901 | Jan 27 01:16PM ]
steve@steve-pc /tmp> g++ -o test.gcc.Os -Os test.cpp
                                                                              [ 0s875 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.O0
0 32764                                                                       [ 0s004 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.O2
0 0                                                                           [ 0s004 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.Os
0 0                                                                           [ 0s003 | Jan 27 01:16PM ]
steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp
                                                                              [ 1s089 | Jan 27 01:17PM ]
steve@steve-pc /tmp> clang++ -o test.clang.Os -Os test.cpp
                                                                              [ 1s058 | Jan 27 01:17PM ]
steve@steve-pc /tmp> clang++ -o test.clang.O2 -O2 test.cpp
                                                                              [ 1s109 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 274247888                                                                   [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.Os
0 0                                                                           [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O2
0 0                                                                           [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 2127532240                                                                  [ 0s002 | Jan 27 01:18PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 344211664                                                                   [ 0s004 | Jan 27 01:18PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 1694408912                                                                  [ 0s004 | Jan 27 01:18PM ]

Jadi di situlah menjadi menarik, itu jelas menunjukkan clang O0 build membaca angka acak, mungkin ruang tumpukan.

Saya segera membuka IDA saya untuk melihat apa yang terjadi:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rax
  __int64 v4; // rax
  int result; // eax
  unsigned int v6; // [rsp+8h] [rbp-18h]
  unsigned int v7; // [rsp+10h] [rbp-10h]
  unsigned __int64 v8; // [rsp+18h] [rbp-8h]

  v8 = __readfsqword(0x28u); // alloca of 0x28
  v7 = 0; // this is foo a{}
  bar::bar((bar *)&v6); // this is bar b{}
  v3 = std::ostream::operator<<(&std::cout, v7); // this is clearly 0
  v4 = std::operator<<<std::char_traits<char>>(v3, 32LL); // 32 = 0x20 = ' '
  result = std::ostream::operator<<(v4, v6); // joined as cout << a.a << ' ' << b.b, so this is reading random values!!
  if ( __readfsqword(0x28u) == v8 ) // stack align check
    result = 0;
  return result;
}

Sekarang, apa bar::bar(bar *this)fungsinya?

void __fastcall bar::bar(bar *this)
{
  ;
}

Hmm, tidak ada. Kami harus menggunakan perakitan:

.text:00000000000011D0                               ; __int64 __fastcall bar::bar(bar *__hidden this)
.text:00000000000011D0                                               public _ZN3barC2Ev
.text:00000000000011D0                               _ZN3barC2Ev     proc near               ; CODE XREF: main+20↓p
.text:00000000000011D0
.text:00000000000011D0                               var_8           = qword ptr -8
.text:00000000000011D0
.text:00000000000011D0                               ; __unwind {
.text:00000000000011D0 55                                            push    rbp
.text:00000000000011D1 48 89 E5                                      mov     rbp, rsp
.text:00000000000011D4 48 89 7D F8                                   mov     [rbp+var_8], rdi
.text:00000000000011D8 5D                                            pop     rbp
.text:00000000000011D9 C3                                            retn
.text:00000000000011D9                               ; } // starts at 11D0
.text:00000000000011D9                               _ZN3barC2Ev     endp

Jadi ya, tidak apa-apa, yang pada dasarnya dilakukan oleh konstruktor this = this. Tapi kita tahu bahwa itu sebenarnya memuat alamat tumpukan acak yang tidak diinisialisasi dan mencetaknya.

Bagaimana jika kita secara eksplisit memberikan nilai untuk kedua struct?

#include <iostream>

struct foo {
    foo() = default;
    int a;
};

struct bar {
    bar();
    int b;
};

bar::bar() = default;

int main() {
    foo a{0};
    bar b{0};
    std::cout << a.a << ' ' << b.b;
}

Hit up clang, oopsie:

steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp
test.cpp:17:9: error: no matching constructor for initialization of 'bar'
    bar b{0};
        ^~~~
test.cpp:8:8: note: candidate constructor (the implicit copy constructor) not viable: no known conversion
      from 'int' to 'const bar' for 1st argument
struct bar {
       ^
test.cpp:8:8: note: candidate constructor (the implicit move constructor) not viable: no known conversion
      from 'int' to 'bar' for 1st argument
struct bar {
       ^
test.cpp:13:6: note: candidate constructor not viable: requires 0 arguments, but 1 was provided
bar::bar() = default;
     ^
1 error generated.
                                                                              [ 0s930 | Jan 27 01:35PM ]

Nasib serupa dengan g ++ juga:

steve@steve-pc /tmp> g++ test.cpp
test.cpp: In function ‘int main()’:
test.cpp:17:12: error: no matching function for call to ‘bar::bar(<brace-enclosed initializer list>)’
     bar b{0};
            ^
test.cpp:8:8: note: candidate: ‘bar::bar()’
 struct bar {
        ^~~
test.cpp:8:8: note:   candidate expects 0 arguments, 1 provided
test.cpp:8:8: note: candidate: ‘constexpr bar::bar(const bar&)’
test.cpp:8:8: note:   no known conversion for argument 1 from ‘int’ to ‘const bar&’
test.cpp:8:8: note: candidate: ‘constexpr bar::bar(bar&&)’
test.cpp:8:8: note:   no known conversion for argument 1 from ‘int’ to ‘bar&&’
                                                                              [ 0s718 | Jan 27 01:35PM ]

Jadi ini berarti inisialisasi langsung secara efektif bar b(0), bukan inisialisasi agregat.

Ini mungkin karena jika Anda tidak menyediakan implementasi konstruktor eksplisit, ini berpotensi menjadi simbol eksternal, misalnya:

bar::bar() {
  this.b = 1337; // whoa
}

Kompilator tidak cukup pintar untuk menyimpulkan ini sebagai panggilan tanpa operasi / sebaris dalam tahap yang tidak dioptimalkan.

Steve Fan
sumber