Apakah mungkin untuk mencegah penghilangan anggota inisialisasi agregat?

43

Saya memiliki struct dengan banyak anggota dengan tipe yang sama, seperti ini

struct VariablePointers {
   VariablePtr active;
   VariablePtr wasactive;
   VariablePtr filename;
};

Masalahnya adalah jika saya lupa menginisialisasi salah satu anggota struct (mis wasactive), seperti ini:

VariablePointers{activePtr, filename}

Kompiler tidak akan mengeluh tentang hal itu, tetapi saya akan memiliki satu objek yang sebagian diinisialisasi. Bagaimana saya bisa mencegah kesalahan semacam ini? Saya bisa menambahkan konstruktor, tetapi itu akan menduplikasi daftar variabel dua kali, jadi saya harus mengetik semua ini tiga kali!

Harap juga tambahkan jawaban C ++ 11 , jika ada solusi untuk C ++ 11 (saat ini saya terbatas pada versi itu). Namun, standar bahasa yang lebih baru juga diterima!

Johannes Schaub - litb
sumber
6
Mengetik konstruktor tidak terdengar terlalu mengerikan. Kecuali jika Anda memiliki terlalu banyak anggota, dalam hal ini, mungkin refactoring sudah beres.
Gonen I
1
@Someprogrammerdude Saya pikir maksudnya kesalahannya adalah Anda secara tidak sengaja dapat menghilangkan nilai inisialisasi
Gonen I
2
@ theWiseBro jika Anda tahu bagaimana array / vektor membantu Anda harus mengirim jawaban. Ini tidak begitu jelas, saya tidak melihatnya
idclev 463035818
2
@Someprogrammerdude Tapi apakah itu bahkan peringatan? Tidak dapat melihatnya dengan VS2019.
acraig5075
8
Ada -Wmissing-field-initializersbendera kompilasi.
Ron

Jawaban:

42

Berikut adalah trik yang memicu kesalahan tautan jika inisialisasi yang diperlukan tidak ada:

struct init_required_t {
    template <class T>
    operator T() const; // Left undefined
} static const init_required;

Pemakaian:

struct Foo {
    int bar = init_required;
};

int main() {
    Foo f;
}

Hasil:

/tmp/ccxwN7Pn.o: In function `Foo::Foo()':
prog.cc:(.text._ZN3FooC2Ev[_ZN3FooC5Ev]+0x12): undefined reference to `init_required_t::operator int<int>() const'
collect2: error: ld returned 1 exit status

Peringatan:

  • Sebelum C ++ 14, ini mencegah Fooagregat sama sekali.
  • Ini secara teknis bergantung pada perilaku tidak terdefinisi (pelanggaran ODR), tetapi harus bekerja pada platform waras.
Quentin
sumber
Anda dapat menghapus operator konversi dan kemudian kesalahan kompiler.
jrok
@ jrok ya, tetapi ini adalah salah satu segera setelah Foodinyatakan, bahkan jika Anda tidak pernah benar-benar memanggil operator.
Quentin
2
@ jrok Tapi kemudian tidak mengkompilasi bahkan jika inisialisasi disediakan. godbolt.org/z/yHZNq_ Tambahan: Untuk MSVC berfungsi seperti yang Anda jelaskan: godbolt.org/z/uQSvDa Apakah ini bug?
n314159
Tentu saja, konyol saya.
jrok
6
Sayangnya, trik ini tidak berfungsi dengan C ++ 11, karena itu akan menjadi non-agregat lalu :( Saya menghapus tag C ++ 11, jadi jawaban Anda juga layak (tolong jangan hapus), tetapi solusi C ++ 11 masih lebih disukai, jika memungkinkan
Johannes Schaub - litb
22

Untuk dentang dan gcc Anda dapat mengkompilasi dengan -Werror=missing-field-initializersyang mengubah peringatan pada inisialisasi bidang yang hilang menjadi kesalahan.godbolt

Sunting: Untuk MSVC, sepertinya tidak ada peringatan yang dipancarkan bahkan di level /Wall, jadi saya rasa tidak mungkin untuk memperingatkan tentang inisialisasi yang hilang dengan kompiler ini. godbolt

n314159
sumber
7

Bukan solusi yang elegan dan praktis, saya kira ... tetapi harus bekerja juga dengan C ++ 11 dan memberikan kesalahan waktu kompilasi (bukan waktu tautan).

Idenya adalah menambahkan di struct Anda anggota tambahan, di posisi terakhir, dari tipe tanpa inisialisasi default (dan yang tidak dapat menginisialisasi dengan nilai tipe VariablePtr (atau apa pun jenis nilai sebelumnya)

Contohnya

struct bar
 {
   bar () = delete;

   template <typename T> 
   bar (T const &) = delete;

   bar (int) 
    { }
 };

struct foo
 {
   char a;
   char b;
   char c;

   bar sentinel;
 };

Dengan cara ini Anda dipaksa untuk menambahkan semua elemen dalam daftar inisialisasi agregat Anda, termasuk nilai untuk secara eksplisit menginisialisasi nilai terakhir (bilangan bulat untuk sentinel, dalam contoh) atau Anda mendapatkan "panggilan ke konstruktor yang dihapus dari kesalahan 'bar'".

Begitu

foo f1 {'a', 'b', 'c', 1};

kompilasi dan

foo f2 {'a', 'b'};  // ERROR

tidak.

Sayangnya juga

foo f3 {'a', 'b', 'c'};  // ERROR

tidak dikompilasi.

- EDIT -

Seperti yang ditunjukkan oleh MSalters (terima kasih) ada cacat (cacat lain) dalam contoh asli saya: barnilai dapat diinisialisasi dengan charnilai (yang dapat dikonversi ke int), jadi bekerja inisialisasi berikut

foo f4 {'a', 'b', 'c', 'd'};

dan ini bisa sangat membingungkan.

Untuk menghindari masalah ini, saya telah menambahkan konstruktor template yang dihapus berikut

 template <typename T> 
 bar (T const &) = delete;

jadi f4deklarasi sebelumnya memberikan kesalahan kompilasi karena dnilainya dicegat oleh konstruktor template yang dihapus

maks66
sumber
Terima kasih, ini bagus! Ini tidak sempurna seperti yang Anda sebutkan, dan juga foo f;gagal membuat kompilasi, tapi mungkin itu lebih merupakan fitur daripada cacat dengan trik ini. Akan menerima jika tidak ada proposal yang lebih baik dari ini.
Johannes Schaub - litb
1
Saya akan membuat konstruktor bilah menerima anggota kelas tetap yang disebut sesuatu seperti init_list_end agar mudah dibaca
Gonen I
@GonenI - untuk keterbacaan, Anda dapat menerima enum , dan nama init_list_end(o hanya list_end) nilai itu enum; tetapi keterbacaan menambah banyak ketikan, jadi, mengingat bahwa nilai tambahan adalah titik lemah dari jawaban ini, saya tidak tahu apakah itu ide yang bagus.
Maks66
Mungkin menambahkan sesuatu seperti constexpr static int eol = 0;di header bar. test{a, b, c, eol}sepertinya cukup mudah dibaca oleh saya.
n314159
@ n314159 - yah ... menjadi bar::eol; hampir melewati enumnilai; tetapi saya tidak berpikir itu penting: inti dari jawabannya adalah "tambahkan di struct Anda anggota tambahan, di posisi terakhir, dari jenis tanpa inisialisasi default"; yang barbagian hanyalah sebuah contoh sepele untuk menunjukkan bahwa solusi bekerja; "tipe tanpa inisialisasi default" yang tepat harus bergantung pada keadaan (IMHO).
Maks66
4

Untuk CppCoreCheck ada aturan untuk memeriksa persis itu, jika semua anggota telah diinisialisasi dan yang dapat diubah dari peringatan menjadi kesalahan - yang biasanya tentu saja di seluruh program.

Memperbarui:

Aturan yang ingin Anda periksa adalah bagian dari typesafety Type.6:

Tipe.6: Selalu menginisialisasi variabel anggota: selalu menginisialisasi, mungkin menggunakan konstruktor default atau inisialisasi anggota default.

sayang
sumber
2

Cara paling sederhana adalah tidak memberikan tipe anggota konstruktor tanpa argumen:

struct B
{
    B(int x) {}
};
struct A
{
    B a;
    B b;
    B c;
};

int main() {

        // A a1{ 1, 2 }; // will not compile 
        A a1{ 1, 2, 3 }; // will compile 

Opsi lain: Jika anggota Anda adalah const &, Anda harus menginisialisasi semuanya:

struct A {    const int& x;    const int& y;    const int& z; };

int main() {

//A a1{ 1,2 };  // will not compile 
A a2{ 1,2, 3 }; // compiles OK

Jika Anda dapat hidup dengan satu const & member dummy, Anda dapat menggabungkannya dengan ide @ max66 tentang seorang penjaga.

struct end_of_init_list {};

struct A {
    int x;
    int y;
    int z;
    const end_of_init_list& dummy;
};

    int main() {

    //A a1{ 1,2 };  // will not compile
    //A a2{ 1,2, 3 }; // will not compile
    A a3{ 1,2, 3,end_of_init_list() }; // will compile

Dari cppreference https://en.cppreference.com/w/cpp/language/aggregate_initialization

Jika jumlah klausul penginisialisasi kurang dari jumlah anggota atau daftar penginisialisasi benar-benar kosong, anggota yang tersisa diinisialisasi nilai. Jika seorang anggota dari tipe referensi adalah salah satu dari anggota yang tersisa ini, program ini salah bentuk.

Pilihan lain adalah mengambil ide sentinel max66 dan menambahkan gula sintaksis agar mudah dibaca

struct init_list_guard
{
    struct ender {

    } static const end;
    init_list_guard() = delete;

    init_list_guard(ender e){ }
};

struct A
{
    char a;
    char b;
    char c;

    init_list_guard guard;
};

int main() {
   // A a1{ 1, 2 }; // will not compile 
   // A a2{ 1, init_list_guard::end }; // will not compile 
   A a3{ 1,2,3,init_list_guard::end }; // compiles OK
Gonen I
sumber
Sayangnya, ini membuat tidak bisa Adigerakkan dan mengubah semantik-copy ( Abukan agregat nilai lagi, jadi untuk berbicara) :(
Johannes Schaub - litb
@ JohannesSchaub-litb OK. Bagaimana dengan gagasan ini dalam jawaban yang saya edit?
Gonen I
@ JohannesSchaub-litb: sama pentingnya, versi pertama menambahkan tingkat tipuan dengan membuat petunjuk anggota. Yang lebih penting lagi, mereka harus menjadi referensi untuk sesuatu, dan 1,2,3benda-benda tersebut secara efektif adalah penduduk lokal dalam penyimpanan otomatis yang keluar dari ruang lingkup ketika fungsi berakhir. Dan itu membuat sizeof (A) 24 bukannya 3 pada sistem dengan pointer 64-bit (seperti x86-64).
Peter Cordes
Referensi dummy meningkatkan ukuran dari 3 hingga 16 byte (padding untuk penjajaran pointer (referensi) anggota + pointer itu sendiri.) Selama Anda tidak pernah menggunakan referensi, mungkin ok jika menunjuk ke objek yang hilang. cakupan. Saya pasti khawatir tentang hal itu tidak mengoptimalkan, dan menyalinnya tentu saja tidak. (Kelas kosong memiliki peluang yang lebih baik untuk mengoptimalkan selain ukurannya, sehingga opsi ketiga di sini adalah yang paling buruk, tetapi masih membutuhkan ruang di setiap objek setidaknya dalam beberapa ABI. Saya juga masih khawatir tentang bantalan yang sakit. optimasi dalam beberapa kasus.)
Peter Cordes