Inisialisasi struct C ++ yang nyaman

141

Saya mencoba menemukan cara yang nyaman untuk menginisialisasi 'pod' struct C ++. Sekarang, pertimbangkan struct berikut:

struct FooBar {
  int foo;
  float bar;
};
// just to make all examples work in C and C++:
typedef struct FooBar FooBar;

Jika saya ingin menginisialisasi ini dengan mudah di C (!), Saya cukup menulis:

/* A */ FooBar fb = { .foo = 12, .bar = 3.4 }; // illegal C++, legal C

Perhatikan bahwa saya ingin secara eksplisit menghindari notasi berikut, karena menurut saya dibuat untuk mematahkan leher saya jika saya mengubah sesuatu di struct di masa depan:

/* B */ FooBar fb = { 12, 3.4 }; // legal C++, legal C, bad style?

Untuk mencapai yang sama (atau paling tidak serupa) di C ++ seperti pada /* A */contoh, saya harus mengimplementasikan konstruktor idiot:

FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) {}
// ->
/* C */ FooBar fb(12, 3.4);

Mana yang baik untuk air mendidih, tetapi tidak cocok untuk orang malas (kemalasan adalah hal yang baik, bukan?). Juga, ini sama buruknya dengan /* B */contoh, karena tidak secara eksplisit menyatakan nilai yang masuk ke anggota mana.

Jadi, pertanyaan saya pada dasarnya adalah bagaimana saya bisa mencapai sesuatu yang mirip /* A */atau lebih baik di C ++? Atau, saya akan baik-baik saja dengan penjelasan mengapa saya tidak ingin melakukan ini (yaitu mengapa paradigma mental saya buruk).

EDIT

Dengan nyaman , maksud saya juga dapat dipertahankan dan tidak berlebihan .

bitmask
sumber
2
Saya pikir contoh B sedekat yang akan Anda dapatkan.
Marlon
2
Saya tidak melihat bagaimana contoh B adalah "gaya buruk." Masuk akal bagi saya, karena Anda menginisialisasi setiap anggota pada gilirannya dengan nilai masing-masing.
Mike Bailey
26
Mike, itu gaya yang buruk karena tidak jelas nilai yang jatuh ke anggota yang mana. Anda harus pergi dan melihat definisi struct dan kemudian menghitung anggota untuk menemukan apa arti setiap nilai.
jnnnnn
9
Plus, jika definisi FooBar berubah di masa depan, inisialisasi dapat menjadi rusak.
Edward Falk
jika inisialisasi menjadi panjang dan kompleks, jangan lupakan pola pembangun
kereta luncur

Jawaban:

20

Inisialisasi yang ditunjuk akan didukung dalam c ++ 2a, tetapi Anda tidak harus menunggu, karena mereka secara resmi didukung oleh GCC, Dentang dan MSVC.

#include <iostream>
#include <filesystem>

struct hello_world {
    const char* hello;
    const char* world;
};

int main () 
{
    hello_world hw = {
        .hello = "hello, ",
        .world = "world!"
    };

    std::cout << hw.hello << hw.world << std::endl;
    return 0;
}

GCC Demo MSVC Demo

ivaigult
sumber
Caveat emptor: perlu diingat bahwa jika Anda menambahkan parameter ke akhir struct nanti, inisialisasi lama masih akan dikompilasi secara diam-diam tanpa harus diinisialisasi.
Catskul
1
@Catskul No. Ini akan diinisialisasi dengan daftar penginisialisasi kosong, yang akan menghasilkan inisialisasi dengan nol.
ivaigult
Kamu benar. Terima kasih. Saya harus mengklarifikasi, parameter yang tersisa akan diam-diam secara default diinisialisasi. Maksud saya maksudkan adalah bahwa siapa pun yang berharap ini dapat membantu menegakkan inisialisasi eksplisit tipe POD akan kecewa.
Catskul
43

Karena style Atidak diizinkan dalam C ++ dan Anda tidak ingin style Blalu bagaimana menggunakan style BX:

FooBar fb = { /*.foo=*/ 12, /*.bar=*/ 3.4 };  // :)

Setidaknya membantu sedikit banyak.

iammilind
sumber
8
+1: itu tidak benar-benar memastikan inisialisasi yang benar (dari POV kompiler) tetapi pasti membantu pembaca ... meskipun komentar harus tetap disinkronkan.
Matthieu M.
18
Komentar tidak mencegah inisialisasi struktur agar tidak rusak jika saya memasukkan bidang baru di antara foodan bardi masa mendatang. C masih akan menginisialisasi bidang yang kita inginkan, tetapi C ++ tidak. Dan ini adalah inti dari pertanyaan - bagaimana mencapai hasil yang sama dalam C ++. Maksudku, Python melakukan ini dengan argumen bernama, C - dengan bidang "bernama", dan C ++ harus memiliki sesuatu juga, saya harap.
dmitry_romanov
2
Komentar sinkron? Beri aku istirahat. Keamanan melewati jendela. Susun ulang parameter dan boom. Jauh lebih baik dengan explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) . Perhatikan kata kunci eksplisit . Bahkan melanggar standar lebih baik dalam hal keamanan. Dalam Dentang: -Wno-c99-extensions
Daniel O
@DanielW, Ini bukan tentang apa yang lebih baik atau apa yang tidak. jawaban ini sesuai dengan OP yang tidak ingin Gaya A (bukan c ++), B atau C, yang mencakup semua kasus yang valid.
iammilind
@ iammilind Saya pikir petunjuk mengapa paradigma mental OP buruk dapat meningkatkan jawabannya. Saya menganggap ini berbahaya seperti sekarang.
Daerst
10

Anda bisa menggunakan lambda:

const FooBar fb = [&] {
    FooBar fb;
    fb.foo = 12;
    fb.bar = 3.4;
    return fb;
}();

Informasi lebih lanjut tentang idiom ini dapat ditemukan di blog Herb Sutter .

bulu mata
sumber
1
Pendekatan tersebut menginisialisasi bidang dua kali. Setelah di konstruktor. Kedua adalah fb.XXX = YYY.
Dmytro Ovdiienko
9

Ekstrak kontants menjadi fungsi yang menggambarkannya (refactoring dasar):

FooBar fb = { foo(), bar() };

Saya tahu bahwa gaya sangat dekat dengan gaya yang tidak ingin Anda gunakan, tetapi memungkinkan penggantian nilai konstan yang lebih mudah dan juga menjelaskannya (sehingga tidak perlu mengedit komentar), jika mereka pernah mengubahnya.

Hal lain yang dapat Anda lakukan (karena Anda malas) adalah membuat konstruktor sebaris, sehingga Anda tidak perlu mengetik sebanyak (menghapus "Foobar ::" dan menghabiskan waktu beralih antara file h dan cpp):

struct FooBar {
  FooBar(int f, float b) : foo(f), bar(b) {}
  int foo;
  float bar;
};
ralphtheninja
sumber
1
Saya sangat merekomendasikan siapa pun yang membaca pertanyaan ini untuk memilih gaya di snipet kode bawah untuk jawaban ini jika semua yang Anda ingin lakukan adalah dapat dengan cepat menginisialisasi struct dengan seperangkat nilai.
kayleeFrye_onDeck
8

Pertanyaan Anda agak sulit karena bahkan fungsinya:

static FooBar MakeFooBar(int foo, float bar);

dapat disebut sebagai:

FooBar fb = MakeFooBar(3.4, 5);

karena aturan promosi dan konversi untuk tipe numerik bawaan. (C tidak pernah diketik dengan sangat kuat)

Di C ++, apa yang Anda inginkan dapat dicapai, meskipun dengan bantuan template dan pernyataan statis:

template <typename Integer, typename Real>
FooBar MakeFooBar(Integer foo, Real bar) {
  static_assert(std::is_same<Integer, int>::value, "foo should be of type int");
  static_assert(std::is_same<Real, float>::value, "bar should be of type float");
  return { foo, bar };
}

Di C, Anda dapat memberi nama parameter, tetapi Anda tidak akan pernah lanjut.

Di sisi lain, jika semua yang Anda inginkan bernama parameter, maka Anda menulis banyak kode rumit:

struct FooBarMaker {
  FooBarMaker(int f): _f(f) {}
  FooBar Bar(float b) const { return FooBar(_f, b); }
  int _f;
};

static FooBarMaker Foo(int f) { return FooBarMaker(f); }

// Usage
FooBar fb = Foo(5).Bar(3.4);

Dan Anda dapat menambahkan perlindungan promosi tipe jika Anda suka.

Matthieu M.
sumber
1
"Dalam C ++, apa yang Anda inginkan dapat dicapai": bukankah OP meminta untuk membantu mencegah campuran urutan parameter? Bagaimana templat yang Anda usulkan akan mencapai itu? Hanya untuk kesederhanaan, katakanlah kita memiliki 2 parameter, keduanya int.
maks
@ Max: Ini akan mencegahnya hanya jika jenisnya berbeda (bahkan jika mereka dapat dikonversi satu sama lain), yang merupakan situasi OP. Jika tidak dapat membedakan jenisnya, maka tentu saja itu tidak berhasil, tapi itu pertanyaan lain.
Matthieu M.
Ah mengerti. Ya, ini adalah dua masalah yang berbeda, dan saya kira yang kedua tidak memiliki solusi yang baik di C ++ saat ini (tetapi tampaknya C ++ 20 menambahkan dukungan untuk nama parameter gaya C99 dalam inisialisasi agregat).
maks
6

Banyak frontend C ++ kompiler (termasuk GCC dan dentang) memahami sintaks initializer C. Jika Anda bisa, cukup gunakan metode itu.

Matthias Urlichs
sumber
16
Yang tidak sesuai dengan standar C ++!
bitmask
5
Saya tahu ini tidak standar. Tetapi jika Anda bisa menggunakannya, itu masih cara yang paling masuk akal untuk menginisialisasi struct.
Matthias Urlichs
2
Anda dapat melindungi jenis x dan y yang membuat konstruktor salah pribadi:private: FooBar(float x, int y) {};
dmitry_romanov
4
dentang (compiler c ++ berbasis llvm) juga mendukung sintaks ini. Sayang sekali itu bukan bagian dari standar.
nimrodm
Kita semua tahu bahwa inisialisasi C bukan bagian dari standar C ++. Tetapi banyak kompiler yang memahaminya dan pertanyaannya tidak mengatakan kompiler mana yang ditargetkan, jika ada. Karena itu tolong jangan downvote jawaban ini.
Matthias Urlichs
4

Namun cara lain dalam C ++ adalah

struct Point
{
private:

 int x;
 int y;

public:
    Point& setX(int xIn) { x = Xin; return *this;}
    Point& setY(int yIn) { y = Yin; return *this;}

}

Point pt;
pt.setX(20).setY(20);
parapura rajkumar
sumber
2
Tidak praktis untuk pemrograman fungsional (yaitu membuat objek dalam daftar argumen pemanggilan fungsi), tetapi sebaliknya ide yang sangat rapi!
bitmask
27
pengoptimal mungkin menguranginya, tetapi mata saya tidak.
Matthieu M.
6
Dua kata: argh ... argh! Bagaimana ini lebih baik daripada menggunakan data publik dengan 'Point pt; pt.x = pt.y = 20; `? Atau jika Anda ingin enkapsulasi, bagaimana ini lebih baik daripada konstruktor?
OldPeculier
3
Ini lebih baik daripada konstruktor karena Anda harus melihat deklarasi konstruktor untuk urutan parameter ... apakah itu x, y atau y, x tetapi cara saya menunjukkannya jelas di situs panggilan
parapura rajkumar
2
Ini tidak berfungsi jika Anda ingin const const. atau jika Anda ingin memberi tahu kompiler untuk tidak mengizinkan struct yang tidak diinisialisasi. Jika Anda benar - benar ingin melakukannya dengan cara ini, setidaknya tandai setter dengan inline!
Matthias Urlichs
3

Opsi D:

FooBar FooBarMake(int foo, float bar)

Legal C, legal C ++. Mudah dioptimalkan untuk POD. Tentu saja tidak ada argumen bernama, tetapi ini seperti semua C ++. Jika Anda ingin argumen bernama, Objective C harus menjadi pilihan yang lebih baik.

Opsi E:

FooBar fb;
memset(&fb, 0, sizeof(FooBar));
fb.foo = 4;
fb.bar = 15.5f;

Legal C, legal C ++. Argumen yang dinamai.

John
sumber
12
Alih-alih memset yang dapat Anda gunakan FooBar fb = {};di C ++, ini secara default menginisialisasi semua anggota struct.
Öö Tiib
@ ÖöTiib: Sayangnya itu ilegal C.
CB Bailey
3

Saya tahu pertanyaan ini sudah lama, tetapi ada cara untuk menyelesaikan ini sampai C ++ 20 akhirnya membawa fitur ini dari C ke C ++. Apa yang dapat Anda lakukan untuk menyelesaikan ini adalah menggunakan macro preprocessor dengan static_asserts untuk memeriksa inisialisasi Anda valid. (Saya tahu makro umumnya buruk, tetapi di sini saya tidak melihat cara lain.) Lihat contoh kode di bawah ini:

#define INVALID_STRUCT_ERROR "Instantiation of struct failed: Type, order or number of attributes is wrong."

#define CREATE_STRUCT_1(type, identifier, m_1, p_1) \
{ p_1 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_2(type, identifier, m_1, p_1, m_2, p_2) \
{ p_1, p_2 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_3(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3) \
{ p_1, p_2, p_3 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\

#define CREATE_STRUCT_4(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3, m_4, p_4) \
{ p_1, p_2, p_3, p_4 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_4) >= (offsetof(type, m_3) + sizeof(identifier.m_3)), INVALID_STRUCT_ERROR);\

// Create more macros for structs with more attributes...

Kemudian ketika Anda memiliki struct dengan atribut const, Anda bisa melakukan ini:

struct MyStruct
{
    const int attr1;
    const float attr2;
    const double attr3;
};

const MyStruct test = CREATE_STRUCT_3(MyStruct, test, attr1, 1, attr2, 2.f, attr3, 3.);

Agak merepotkan, karena Anda memerlukan makro untuk setiap kemungkinan jumlah atribut dan Anda perlu mengulangi jenis dan nama instance Anda dalam panggilan makro. Anda juga tidak dapat menggunakan makro dalam pernyataan pengembalian, karena pernyataan muncul setelah inisialisasi.

Tetapi itu memecahkan masalah Anda: Ketika Anda mengubah struct, panggilan akan gagal pada waktu kompilasi.

Jika Anda menggunakan C ++ 17, Anda bahkan dapat membuat makro ini lebih ketat dengan memaksa jenis yang sama, misalnya:

#define CREATE_STRUCT_3(type, identifier, m_1, p_1, m_2, p_2, m_3, p_3) \
{ p_1, p_2, p_3 };\
static_assert(offsetof(type, m_1) == 0, INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_2) >= sizeof(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(offsetof(type, m_3) >= (offsetof(type, m_2) + sizeof(identifier.m_2)), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_1) == typeid(identifier.m_1), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_2) == typeid(identifier.m_2), INVALID_STRUCT_ERROR);\
static_assert(typeid(p_3) == typeid(identifier.m_3), INVALID_STRUCT_ERROR);\
Max Vollmer
sumber
Apakah ada proposal C ++ 20 untuk mengizinkan inisialisasi bernama?
Maël Nison
1
@ MaëlNison Ya: Inisialisasi yang ditunjuk (sejak C ++ 20)
Max Vollmer
2

Jalan /* B */ ini baik-baik saja di C ++ juga C ++ 0x akan memperpanjang sintaks sehingga berguna untuk wadah C ++ juga. Saya tidak mengerti mengapa Anda menyebutnya gaya buruk?

Jika Anda ingin menunjukkan parameter dengan nama maka Anda dapat menggunakan pustaka parameter boost , tetapi itu mungkin membingungkan seseorang yang tidak terbiasa dengannya.

Menyusun ulang anggota struct seperti mengatur ulang parameter fungsi, refactoring seperti itu dapat menyebabkan masalah jika Anda tidak melakukannya dengan sangat hati-hati.

Öö Tiib
sumber
7
Saya menyebutnya gaya buruk karena saya pikir ini tidak dapat dipertahankan. Bagaimana jika saya menambah anggota lain dalam setahun? Atau jika saya mengubah pemesanan / jenis anggota? Setiap bagian dari kode yang menginisialisasi mungkin (sangat mungkin) rusak.
bitmask
2
@ Bitmask Tapi selama Anda tidak menyebutkan argumen, Anda harus memperbarui panggilan konstruktor juga, dan saya pikir tidak banyak orang berpikir konstruktor adalah gaya buruk yang tidak dapat dipertahankan. Saya juga berpikir inisialisasi bernama bukan C, tapi C99, yang C ++ jelas bukan superset.
Christian Rau
2
Jika Anda menambahkan anggota lain dalam satu tahun ke akhir struct maka akan diinisialisasi-default dalam kode yang sudah ada. Jika Anda memesan ulang maka Anda harus mengedit semua kode yang ada, tidak ada hubungannya.
Öö Tiib
1
@ Bitmask: Contoh pertama akan "tidak dapat dipelihara" juga. Apa yang terjadi jika Anda mengganti nama variabel dalam struct? Tentu, Anda bisa melakukan penggantian-semua, tapi itu bisa secara tidak sengaja mengubah nama variabel yang tidak boleh diganti namanya.
Mike Bailey
@ChristianRau Sejak kapan C99 bukan C? Bukankah C grup dan C99 versi tertentu / spesifikasi ISO?
altendky
1

Bagaimana dengan sintaks ini?

typedef struct
{
    int a;
    short b;
}
ABCD;

ABCD abc = { abc.a = 5, abc.b = 7 };

Baru saja diuji pada Microsoft Visual C ++ 2015 dan pada g ++ 6.0.2. Bekerja dengan baik.
Anda dapat membuat makro spesifik juga jika Anda ingin menghindari duplikasi nama variabel.

cls
sumber
clang++3.5.0-10 dengan -Weverything -std=c++1zsepertinya mengkonfirmasi itu. Tapi itu tidak beres. Apakah Anda tahu di mana standar menegaskan bahwa ini adalah C ++ yang valid?
bitmask
Saya tidak tahu, tapi saya sudah menggunakannya dalam kompiler yang berbeda sejak lama dan tidak melihat masalah. Sekarang diuji pada g ++ 4.4.7 - berfungsi dengan baik.
cls
5
Saya tidak berpikir ini bekerja. Coba ABCD abc = { abc.b = 7, abc.a = 5 };.
raymai97
@desain, ini berfungsi karena bidang diinisialisasi dengan nilai, dikembalikan oleh operator =. Jadi, sebenarnya Anda menginisialisasi anggota kelas dua kali.
Dmytro Ovdiienko
1

Bagi saya cara paling malas untuk memungkinkan inzialisasi inline adalah menggunakan makro ini.

#define METHOD_MEMBER(TYPE, NAME, CLASS) \
CLASS &set_ ## NAME(const TYPE &_val) { NAME = _val; return *this; } \
TYPE NAME;

struct foo {
    METHOD_MEMBER(string, attr1, foo)
    METHOD_MEMBER(int, attr2, foo)
    METHOD_MEMBER(double, attr3, foo)
};

// inline usage
foo test = foo().set_attr1("hi").set_attr2(22).set_attr3(3.14);

Makro itu menciptakan atribut dan metode referensi diri.

Emanuele Pavanello
sumber