Wygodna inicjalizacja struktury w C ++


141

Próbuję znaleźć wygodny sposób na zainicjowanie struktur „pod” C ++. Rozważmy teraz następującą strukturę:

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

Jeśli chcę wygodnie zainicjalizować to w C (!), Mógłbym po prostu napisać:

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

Zauważ, że chcę wyraźnie uniknąć następującego zapisu, ponieważ wydaje mi się, że skręca mi kark, jeśli zmienię cokolwiek w strukturze w przyszłości:

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

Aby osiągnąć to samo (lub przynajmniej podobne) w C ++ jak w /* A */przykładzie, musiałbym zaimplementować idiotycznego konstruktora:

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

Co jest dobre do gotowania wody, ale nieodpowiednie dla leniwych (lenistwo to dobra rzecz, prawda?). Ponadto jest tak samo zły jak /* B */przykład, ponieważ nie określa wyraźnie, która wartość trafia do którego elementu.

Więc moje pytanie brzmi: jak mogę osiągnąć coś podobnego /* A */lub lepszego w C ++? Alternatywnie, wystarczyłoby mi wyjaśnienie, dlaczego nie powinienem tego robić (tj. Dlaczego mój paradygmat mentalny jest zły).

EDYTOWAĆ

Przez wygodny mam na myśli również konserwowalne i nienadmiarowe .


2
Myślę, że przykład B jest tak bliski, jak zamierzasz.
Marlon

2
Nie rozumiem, jak przykład B to „zły styl”. Ma to dla mnie sens, ponieważ inicjalizujesz każdego członka po kolei z ich odpowiednimi wartościami.
Mike Bailey

26
Mike, to zły styl, ponieważ nie jest jasne, jaka wartość przypada danemu członkowi. Musisz przyjrzeć się definicji struktury, a następnie policzyć członków, aby dowiedzieć się, co oznacza każda wartość.
jnnnnn

9
Ponadto, jeśli definicja FooBar miałaby się zmienić w przyszłości, inicjalizacja może zostać zerwana.
Edward Falk,

jeśli inicjalizacja stanie się długa i złożona, nie zapomnij o wzorcu konstruktora
sanki

Odpowiedzi:


20

Wyznaczone inicjalizacje będą obsługiwane w c ++ 2a, ale nie musisz czekać, ponieważ są oficjalnie obsługiwane przez GCC, Clang i 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


Caveat emptor: pamiętaj, że jeśli później dodasz parametry na końcu struktury, stare inicjalizacje będą nadal dyskretnie kompilować się bez inicjalizacji.
Catskul

1
@Catskul Nie. Zostanie zainicjowany z pustą listą inicjalizacyjną, co spowoduje inicjalizację z zerem.
ivaigult

Masz rację. Dziękuję Ci. Powinienem wyjaśnić, że pozostałe parametry zostaną po cichu skutecznie zainicjowane domyślnie. Chodziło mi o to, że każdy, kto ma nadzieję, że może to pomóc w wymuszeniu pełnej jawnej inicjalizacji typów POD, będzie rozczarowany.
Catskul

43

Ponieważ style Anie jest to dozwolone w C ++ i nie chcesz, style Bto co powiesz na użycie style BX:

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

Przynajmniej w jakimś stopniu pomóc.


8
+1: tak naprawdę nie zapewnia poprawnej inicjalizacji (z punktu widzenia kompilatora), ale z pewnością pomaga czytelnikowi ... chociaż komentarze powinny być zsynchronizowane.
Matthieu M.

18
Komentarz nie zapobiega przerwaniu inicjalizacji struktury, jeśli wstawię nowe pole pomiędzy fooiw barprzyszłości. C nadal zainicjowałby żądane pola, ale C ++ nie. I o to chodzi w pytaniu - jak osiągnąć ten sam wynik w C ++. Mam na myśli, że Python robi to z nazwanymi argumentami, C - z „nazwanymi” polami, a C ++ też powinien mieć coś, mam nadzieję.
dmitry_romanov

2
Komentarze zsynchronizowane? Daj mi spokój. Bezpieczeństwo przechodzi przez okno. Zmień kolejność parametrów i wysięgnika. Znacznie lepiej z explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) . Zwróć uwagę na wyraźne słowo kluczowe. Nawet złamanie normy jest lepsze pod względem bezpieczeństwa. In Clang: -Wno-c99-extensions
Daniel O

@DanielW, Nie chodzi o to, co jest lepsze, a co nie. ta odpowiedź zgodnie z tym, że OP nie chce stylu A (nie c ++), B lub C, który obejmuje wszystkie ważne przypadki.
iammilind

@iammilind Myślę, że wskazówka, dlaczego paradygmat mentalny OP jest zły, może poprawić odpowiedź. Uważam to za niebezpieczne, tak jak jest teraz.
Daerst

10

Możesz użyć lambdy:

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

Więcej informacji na temat tego idiomu można znaleźć na blogu Herba Suttera .


1
Takie podejście inicjuje pola dwukrotnie. Raz w konstruktorze. Po drugie fb.XXX = YYY.
Dmytro Ovdiienko

9

Wyodrębnij składowe do funkcji, które je opisują (podstawowa refaktoryzacja):

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

Wiem, że styl jest bardzo zbliżony do tego, którego nie chciałeś używać, ale umożliwia łatwiejszą zamianę stałych wartości, a także ich wyjaśnienie (dzięki czemu nie trzeba edytować komentarzy), jeśli kiedykolwiek się zmienią.

Inną rzeczą, którą możesz zrobić (ponieważ jesteś leniwy), jest wstawienie konstruktora w tekście, więc nie musisz wpisywać tak dużo (usunięcie "Foobar ::" i czasu spędzonego na przełączaniu między plikiem h i cpp):

struct FooBar {
  FooBar(int f, float b) : foo(f), bar(b) {}
  int foo;
  float bar;
};

1
Gorąco polecam każdemu, kto czyta to pytanie, aby wybrał styl z dolnego fragmentu kodu dla tej odpowiedzi, jeśli wszystko, czego szukasz, to możliwość szybkiego zainicjowania struktur z zestawem wartości.
kayleeFrye_onDeck

8

Twoje pytanie jest nieco trudne, ponieważ nawet funkcja:

static FooBar MakeFooBar(int foo, float bar);

można nazwać:

FooBar fb = MakeFooBar(3.4, 5);

ze względu na zasady promocji i konwersji dla wbudowanych typów liczbowych. (C nigdy nie było naprawdę mocno wpisane)

W C ++ to, co chcesz, jest osiągalne, chociaż za pomocą szablonów i statycznych asercji:

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 };
}

W C możesz nazwać parametry, ale nigdy nie przejdziesz dalej.

Z drugiej strony, jeśli chcesz tylko nazwać parametry, piszesz dużo nieporęcznego kodu:

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);

I jeśli chcesz, możesz zabezpieczyć się przed awansem typu.


1
„W C ++ to, co chcesz, jest osiągalne”: czy OP nie prosił o pomoc w zapobieganiu pomieszaniu kolejności parametrów? W jaki sposób proponowany przez Ciebie szablon mógłby to osiągnąć? Dla uproszczenia załóżmy, że mamy 2 parametry, oba są int.
maks.

@max: Zapobiega temu tylko wtedy, gdy typy różnią się (nawet jeśli są wymienialne na siebie), co jest sytuacją OP. Jeśli nie potrafi rozróżnić typów, to oczywiście nie działa, ale to zupełnie inna kwestia.
Matthieu M.

Ach, rozumiem. Tak, to są dwa różne problemy i wydaje mi się, że drugi nie ma obecnie dobrego rozwiązania w C ++ (ale wygląda na to, że C ++ 20 dodaje obsługę nazw parametrów w stylu C99 w inicjalizacji agregacji).
maksymalnie

6

Wiele nakładek C ++ kompilatorów (w tym GCC i clang) rozumie składnię inicjalizatora C. Jeśli możesz, po prostu użyj tej metody.


16
Co nie jest zgodne ze standardem C ++!
maska ​​bitowa

5
Wiem, że to niestandardowe. Ale jeśli możesz go użyć, nadal jest to najbardziej rozsądny sposób na zainicjowanie struktury.
Matthias Urlichs

2
Możesz zabezpieczyć typy x i y, czyniąc niewłaściwego konstruktora prywatnym:private: FooBar(float x, int y) {};
dmitry_romanov

4
clang (kompilator C ++ oparty na llvm) również obsługuje tę składnię. Szkoda, że ​​to nie jest część standardu.
nimrodm

Wszyscy wiemy, że inicjatory C nie są częścią standardu C ++. Ale wielu kompilatorów to rozumie, a pytanie nie mówiło, który kompilator jest celem, jeśli w ogóle. Dlatego proszę nie lekceważyć tej odpowiedzi.
Matthias Urlichs

4

Jeszcze innym sposobem w C ++ jest

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);

2
Uciążliwe dla programowania funkcjonalnego (np. Tworzenie obiektu na liście argumentów wywołania funkcji), ale w przeciwnym razie naprawdę fajny pomysł!
maska ​​bitowa

27
optymalizator prawdopodobnie go zmniejsza, ale moje oczy nie.
Matthieu M.

6
Dwa słowa: argh ... argh! Jak to jest lepsze niż używanie danych publicznych z „Point pt; pt.x = pt.y = 20; `? A jeśli chcesz hermetyzacji, czy to jest lepsze niż konstruktor?
OldPeculier

3
To jest lepsze niż konstruktor, ponieważ musisz spojrzeć na deklarację konstruktora dla kolejności parametrów ... czy to jest x, y czy y, x ale sposób, w jaki to pokazałem, jest widoczny w miejscu wywołania
parapura rajkumar

2
To nie działa, jeśli chcesz mieć strukturę const. lub jeśli chcesz powiedzieć kompilatorowi, aby nie zezwalał na niezainicjowane struktury. Jeśli naprawdę chcesz to zrobić w ten sposób, zaznacz przynajmniej setery inline!
Matthias Urlichs

3

Opcja D:

FooBar FooBarMake(int foo, float bar)

Prawne C, prawne C ++. Łatwa optymalizacja dla POD. Oczywiście nie ma nazwanych argumentów, ale to jest jak we wszystkich C ++. Jeśli potrzebujesz nazwanych argumentów, lepszym wyborem powinien być Cel C.

Opcja E:

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

Prawne C, prawne C ++. Nazwane argumenty.


12
Zamiast memset, którego możesz użyć FooBar fb = {};w C ++, domyślnie inicjalizuje wszystkie składowe struktury.
Öö Tiib

@ ÖöTiib: Niestety to jest nielegalne C.
CB Bailey

3

Wiem, że to pytanie jest stare, ale istnieje sposób na rozwiązanie tego problemu, dopóki C ++ 20 w końcu nie przeniesie tę funkcję z C do C ++. Aby rozwiązać ten problem, użyj makr preprocesora z static_asserts, aby sprawdzić, czy inicjalizacja jest prawidłowa. (Wiem, że makra są generalnie złe, ale tutaj nie widzę innego sposobu.) Zobacz przykładowy kod poniżej:

#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...

Następnie, gdy masz strukturę z atrybutami const, możesz zrobić to:

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.);

Jest to trochę niewygodne, ponieważ potrzebujesz makr dla każdej możliwej liczby atrybutów i musisz powtórzyć typ i nazwę swojej instancji w wywołaniu makra. Nie można również użyć makra w instrukcji return, ponieważ potwierdzenia pojawiają się po inicjalizacji.

Ale to rozwiązuje twój problem: kiedy zmienisz strukturę, wywołanie zakończy się niepowodzeniem w czasie kompilacji.

Jeśli używasz C ++ 17, możesz nawet uczynić te makra bardziej rygorystycznymi, wymuszając te same typy, np .:

#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);\

Czy istnieje propozycja C ++ 20 zezwalająca na nazwane inicjatory?
Maël Nison


2

Droga /* B */ jest w porządku w C ++, również C ++ 0x rozszerzy składnię, więc jest przydatny również dla kontenerów C ++. Nie rozumiem, dlaczego nazywasz to złym stylem?

Jeśli chcesz oznaczyć parametry nazwami, możesz użyć biblioteki parametrów boost , ale może to zmylić kogoś nieznającego się na niej.

Zmiana kolejności elementów strukturalnych jest podobna do zmiany kolejności parametrów funkcji. Taka refaktoryzacja może powodować problemy, jeśli nie zrobisz tego bardzo ostrożnie.


7
Nazywam to złym stylem, ponieważ uważam, że nie da się go utrzymać. A jeśli dodam kolejnego członka w ciągu roku? Lub jeśli zmienię kolejność / typy członków? Każdy inicjalizujący się fragment kodu może (bardzo prawdopodobne) się zepsuć.
maska ​​bitowa

2
@bitmask Ale tak długo, jak nie masz nazwanych argumentów, będziesz musiał również aktualizować wywołania konstruktorów i myślę, że niewiele osób uważa, że ​​konstruktory są nie do utrzymania w złym stylu. Myślę również, że nazwana inicjalizacja to nie C, ale C99, którego C ++ zdecydowanie nie jest nadzbiorem.
Christian Rau

2
Jeśli dodasz innego członka w ciągu roku do końca struktury, zostanie ona domyślnie zainicjowana w już istniejącym kodzie. Jeśli zmienisz ich kolejność, będziesz musiał edytować cały istniejący kod, nic do zrobienia.
Öö Tiib

1
@bitmask: Pierwszy przykład również byłby „nie do utrzymania”. Co się stanie, jeśli zamiast tego zmienisz nazwę zmiennej w strukturze? Jasne, możesz wykonać zamianę wszystkiego, ale może to przypadkowo zmienić nazwę zmiennej, której nazwy nie należy zmieniać.
Mike Bailey

@ChristianRau Od kiedy C99 nie jest C? Czy C to nie grupa, a C99 to konkretna wersja / specyfikacja ISO?
altendky

1

A co z tą składnią?

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

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

Właśnie przetestowano na Microsoft Visual C ++ 2015 i g ++ 6.0.2. Działa OK.
Możesz utworzyć określone makro również, jeśli chcesz uniknąć powielania nazwy zmiennej.


clang++3.5.0-10 z -Weverything -std=c++1zwydaje się to potwierdzać. Ale to nie wygląda dobrze. Czy wiesz, gdzie standard potwierdza, że ​​jest to poprawne C ++?
maska ​​bitów

Nie wiem, ale od dawna używałem tego w różnych kompilatorach i nie widziałem żadnych problemów. Teraz testowany na g ++ 4.4.7 - działa dobrze.
cls

5
Nie sądzę, żeby to działało. Spróbuj ABCD abc = { abc.b = 7, abc.a = 5 };.
raymai97

@deselect, działa, ponieważ pole jest inicjalizowane wartością, zwracaną przez operator =. Tak więc w rzeczywistości dwukrotnie inicjalizujesz członka klasy.
Dmytro Ovdiienko

1

Dla mnie najbardziej leniwym sposobem na umożliwienie inicjalizacji inline jest użycie tego makra.

#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);

To makro tworzy atrybut i metodę odniesienia do siebie.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.