Зручна ініціалізація C ++


141

Я намагаюся знайти зручний спосіб ініціалізувати структури "pod" C ++. Тепер розглянемо таку структуру:

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

Якщо я хочу зручно ініціалізувати це на C (!), Я можу просто написати:

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

Зауважте, що я хочу явно уникати наступних позначень, тому що це вражає мене так, як мене роблять зламати шию, якщо я в майбутньому щось зміню в структурі:

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

Щоб досягти такого ж (або принаймні подібного) в C ++, як у /* A */прикладі, мені доведеться реалізувати ідіотичний конструктор:

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

Що добре для киплячої води, але не підходить для ледачих людей (лінь - це добра справа, правда?). Крім того, це майже так само погано, як /* B */приклад, оскільки він не чітко вказує, яке значення йде для якого члена.

Отже, моє питання полягає в основному, як я можу досягти чогось подібного /* A */або кращого в C ++? Як варіант, мені було б добре з поясненням, чому я не хочу цього робити (тобто чому моя психічна парадигма погана).

EDIT

Під зручним , я маю на увазі також ремонтопридатне та непотрібне .


2
Я думаю, що приклад B настільки близький, як ви збираєтеся отримати.
Марлон

2
Я не бачу, як приклад B "поганий стиль". Для мене це має сенс, оскільки ви ініціалізуєте кожного члена по черзі з їх відповідними значеннями.
Майк Бейлі

26
Майк, це поганий стиль, тому що незрозуміло, яке значення йде для якого члена. Вам потрібно перейти до визначення структури, а потім порахувати членів, щоб знайти, що означає кожне значення.
jnnnnn

9
Плюс, якщо визначення FooBar в майбутньому зміниться, ініціалізація може стати порушеною.
Едвард Фолк

якщо ініціалізація стає довгою і складною, не забувайте про схему будівельника
сани

Відповіді:


20

Зазначені ініціалізації будуть підтримуватися в c ++ 2a, але вам не доведеться чекати, оскільки вони офіційно підтримуються GCC, Clang та 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: майте на увазі, що якщо ви додасте параметри до кінця структури пізніше, старі ініціалізації все ще мовчки компілюються без ініціалізації.
Catskul

1
@Catskul Ні. Він буде ініціалізований порожнім списком ініціалізатора, що призведе до ініціалізації з нулем.
ivaigult

Ти маєш рацію. Дякую. Слід уточнити, що інші параметри будуть мовчки ефективно ініціалізовані за замовчуванням. Я хотів сказати, що той, хто сподівається, що це може допомогти застосувати повну явну ініціалізацію типів POD, буде розчарований.
Кацкуль

43

Оскільки style Aце не дозволено в C ++, і ви не хочете, style Bяк тоді використовувати style BX:

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

Принаймні допомогти в якійсь мірі.


8
+1: він насправді не забезпечує правильну ініціалізацію (від компілятора POV), але впевнено допомагає читачеві ... хоча коментарі слід зберігати синхронізовано.
Матьє М.

18
Коментар не перешкоджає порушенню ініціалізації структури, якщо я вставляю нове поле між fooі barв майбутньому. C все одно буде ініціалізувати потрібні поля, але C ++ не буде. І в цьому полягає питання - як досягти такого ж результату в C ++. Я маю на увазі, Python робить це з названими аргументами, C - з полями "найменування", і у C ++ теж має бути щось, я сподіваюся.
dmitry_romanov

2
Коментарі синхронізовані? Дай мені перерву. Безпека йде через вікно. Впорядкуйте параметри та стрілу. Набагато краще з explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) . Зверніть увагу на явне ключове слово. Навіть порушення стандарту краще щодо безпеки. У Кланг: -Wno-c99-розширення
Даніель О

@DanielW, справа не в тому, що краще або що ні. ця відповідь відповідає тому, що ОП не бажає стилю A (не c ++), B або C, який охоплює всі дійсні випадки.
iammilind

@iammilind Я думаю, що підказка, чому психічна парадигма ОП погана, може покращити відповідь. Я вважаю це небезпечним, як зараз.
Даерст

10

Ви можете використовувати лямбда:

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

Більше інформації про цю ідіому можна знайти в блозі Herb Sutter .


1
Такий підхід ініціалізує поля двічі. Одного разу в конструкторі. Друге - це fb.XXX = YYY.
Дмитро Овдієнко

9

Витягніть домішки до функцій, які описують їх (базовий рефакторинг):

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

Я знаю, що стиль дуже близький до того, який ви не хотіли використовувати, але він дозволяє простіше замінити постійні значення, а також пояснити їх (таким чином, не потрібно редагувати коментарі), якщо вони коли-небудь змінюють це.

Інша річ, яку ви можете зробити (оскільки ви ліниві) - це зробити конструктор вбудованим, тому вам не доведеться набирати стільки (видаляючи "Foobar ::", так і витрачаючи час на перемикання між файлом h і cpp):

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

1
Я настійно рекомендую всім, хто читає це питання, вибрати стиль у нижньому фрагменті коду для цієї відповіді, якщо все, що ви хочете зробити, зможе швидко ініціалізувати структури з набором значень.
kayleeFrye_onDeck

8

Ваше запитання дещо складне, оскільки навіть функція:

static FooBar MakeFooBar(int foo, float bar);

може називатися:

FooBar fb = MakeFooBar(3.4, 5);

через правила просування та перетворення для вбудованих числових типів. (C ніколи не був дуже сильно набраний)

У C ++ те, що ви хочете, можна досягти, хоча за допомогою шаблонів і статичних тверджень:

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

В C ви можете назвати параметри, але далі ви ніколи не дістанетесь.

З іншого боку, якщо все, що ви хочете, називається параметрами, то ви пишете безліч громіздких кодів:

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

А ви можете поперчити в захисті просування типу, якщо хочете.


1
"У C ++ те, що ви хочете, є досяжним": чи не ОП просило запобігти змішанню порядку параметрів? Яким чином запропонований вами шаблон може цього досягти? Для простоти, скажімо, у нас є 2 параметри, обидва вони int.
макс

@max: Це запобіжить це лише в тому випадку, якщо типи відрізняються (навіть якщо вони конвертовані один в одного), що є ситуацією з ОП. Якщо він не може виділити типи, то, звичайно, це не працює, але це зовсім інше питання.
Матьє М.

Ах зрозумів. Так, це дві різні проблеми, і я думаю, що друга наразі не має гарного рішення в C ++ (але, схоже, C ++ 20 додає підтримку імен параметрів стилю C99 в сукупній ініціалізації).
макс

6

Багато Frontends C ++ компіляторів (включаючи GCC та clang) розуміють синтаксис ініціалізатора C. Якщо можете, просто використовуйте цей метод.


16
Що не відповідає стандарту C ++!
бітмаска

5
Я знаю, що це нестандартно. Але якщо ви можете використовувати його, це все-таки найрозумніший спосіб ініціалізації структури.
Маттіас Урліхс

2
Ви можете захистити типи x та y, роблячи неправильні конструктори приватними:private: FooBar(float x, int y) {};
dmitry_romanov

4
clang (компілятор c ++ на основі llvm) також підтримує цей синтаксис. Шкода, що це не є частиною стандарту.
nimrodm

Всі ми знаємо, що ініціалізатори C не входять до стандарту C ++. Але багато компіляторів це розуміють, і питання не говорить про те, на який компілятор націлено, якщо такий є. Тому, будь ласка, не оскаржуйте цю відповідь.
Маттіас Урліхс

4

Ще один спосіб у C ++ є

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
Напружене для функціонального програмування (тобто створення об'єкта в списку аргументів виклику функції), але насправді акуратна ідея в іншому випадку!
бітмаска

27
оптимізатор, ймовірно, зменшує його, але мої очі цього не роблять.
Матьє М.

6
Два слова: argh ... argh! Наскільки це краще, ніж використання загальнодоступних даних за допомогою „Point pt; pt.x = pt.y = 20; `? Або якщо ви хочете інкапсуляції, як це краще, ніж конструктор?
OldPeculier

3
Це краще, ніж конструктор, тому що ви повинні подивитися на декларацію конструктора для порядку встановлення параметрів ... це x, y чи y, x, але спосіб, який я це показав, це очевидно на сайті виклику
parapura rajkumar

2
Це не працює, якщо ви хочете структуру const. або якщо ви хочете сказати компілятору не допускати неініціалізованих структур. Якщо ви дійсно хочете зробити це так, принаймні позначте сетерів за допомогою inline!
Маттіас Урліхс

3

Варіант D:

FooBar FooBarMake(int foo, float bar)

Юридичний C, юридичний C ++. Легко оптимізується для POD. Звичайно, ніяких названих аргументів немає, але це як у всіх C ++. Якщо ви хочете назвати аргументи, об'єктив C повинен бути кращим вибором.

Варіант E:

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

Юридичний C, юридичний C ++. Названі аргументи.


12
Замість мемсета, який ви можете використовувати FooBar fb = {};в C ++, він за замовчуванням ініціалізує всі члени структури.
Öö Tiib

@ ÖöTiib: Хоча це, на жаль, незаконне C.
CB Bailey

3

Я знаю, що це питання давнє, але є спосіб вирішити це питання, поки C ++ 20 нарешті не переведе цю функцію з C на C ++. Що ви можете зробити для вирішення цього питання - це використовувати макроси препроцесора з static_asserts, щоб перевірити, чи ваша ініціалізація є дійсною. (Я знаю, що макроси, як правило, погані, але тут я не бачу іншого способу.) Дивіться приклад коду нижче:

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

Тоді, коли у вас є структура з атрибутами const, ви можете це зробити:

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

Це трохи незручно, тому що вам потрібні макроси для кожної можливої ​​кількості атрибутів, і вам потрібно повторити тип і ім'я свого примірника у виклику макросу. Також ви не можете використовувати макрос у зворотному операторі, тому що твердження з’являються після ініціалізації.

Але це все-таки вирішує вашу проблему: Коли ви зміните структуру, виклик буде невдалим під час компіляції.

Якщо ви використовуєте C ++ 17, ви навіть можете зробити ці макроси більш суворими, форсуючи ті самі типи, наприклад:

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

Чи є пропозиція C ++ 20, щоб дозволити названі ініціалізатори?
Maël Nison


2

Шлях /* B */добре в C ++, також C ++ 0x збирається розширити синтаксис, тому він корисний і для контейнерів C ++. Я не розумію, чому ви називаєте це поганим стилем?

Якщо ви хочете вказати параметри з іменами, ви можете використовувати бібліотеку параметрів boost , але це може збентежити когось незнайомого з ним.

Упорядкування членів структури - це як переупорядкування параметрів функції, таке рефакторинг може спричинити проблеми, якщо ви не будете робити це дуже обережно.


7
Я називаю це поганим стилем, тому що я вважаю, що це нульове обслуговування. Що робити, якщо я додам іншого члена через рік? Або якщо я зміню впорядкування / типи членів? Кожен фрагмент коду, який ініціалізується, може (дуже ймовірно) зламатися.
бітмаска

2
@bitmask Але поки ви не маєте назви аргументів, вам також доведеться оновити виклики конструктора, і я думаю, що не багато людей вважають, що конструктори є незрозумілим поганим стилем. Я також думаю, що названа ініціалізація - це не C, а C99, C ++ - це точно не суперсет.
Крістіан Рау

2
Якщо ви додасте іншого члена через рік до кінця структури, він буде ініціалізований за замовчуванням у вже існуючому коді. Якщо їх переупорядкувати, то вам доведеться редагувати весь існуючий код, нічого робити.
Öö Tiib

1
@bitmask: Перший приклад був би також "нездійсненним". Що станеться, якщо замість цього перейменувати змінну в структуру? Звичайно, ви можете зробити заміну всіх, але це може випадково перейменувати змінну, яку не слід перейменовувати.
Майк Бейлі

@ChristianRau З тих пір, коли C99 не C? Чи не група C та C99 - це конкретна версія / специфікація ISO?
altendky

1

Що з цим синтаксисом?

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

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

Щойно перевірена на Microsoft Visual C ++ 2015 та на g ++ 6.0.2. Працює нормально.
Ви можете зробити певний макрос також, якщо хочете уникнути дублювання імені змінної.


clang++3.5.0-10 з, -Weverything -std=c++1zздається, це підтверджують. Але це не виглядає правильно. Чи знаєте ви, де стандарт підтверджує, що це дійсно C ++?
бітмаска

Не знаю, але я використовував це в різних компіляторах давно і не бачив жодних проблем. Зараз тестується на g ++ 4.4.7 - працює чудово.
cls

5
Я не думаю, що це працює. Спробуйте ABCD abc = { abc.b = 7, abc.a = 5 };.
raymai97

@deselect, воно працює, тому що поле ініціалізується зі значенням, повертається оператором =. Отже, ви насправді два рази ініціалізуєте члена класу.
Дмитро Овдієнко

1

Для мене самий ледачий спосіб дозволити вбудовану ініціалізацію - це використовувати цей макрос.

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

Цей макрос створює атрибут та метод самостійного посилання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.