Удобная инициализация структуры C ++

145

Я пытаюсь найти удобный способ инициализировать структуры C ++ «pod». Теперь рассмотрим следующую структуру:

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 ++? В качестве альтернативы, я был бы согласен с объяснением, почему я не должен этого делать (т.е. почему моя ментальная парадигма плохая).

РЕДАКТИРОВАТЬ

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

битовая маска
источник
2
Я думаю, что пример B настолько близок, насколько вы можете себе представить.
Марлон
2
Я не понимаю, насколько пример Б «плохой стиль». Для меня это имеет смысл, поскольку вы инициализируете каждый член по очереди с их соответствующими значениями.
Майк Бейли
27
Майк, это плохой стиль, потому что неясно, какое значение принадлежит какому члену. Вы должны пойти и посмотреть определение структуры, а затем посчитать члены, чтобы узнать, что означает каждое значение.
jnnnnn
9
Кроме того, если определение FooBar изменится в будущем, инициализация может нарушиться.
Эдвард Фальк,
если инициализация становится долгой и сложной, не забывайте о шаблоне строителя
sled

Ответы:

23

Назначенные инициализации будут поддерживаться в 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

ivaigult
источник
Caveat emptor: имейте в виду, что если вы добавите параметры в конец структуры позже, старые инициализации все равно будут компилироваться без уведомления без инициализации.
Catskul
1
@Catskul Нет. Он будет инициализирован пустым списком инициализаторов, что приведет к инициализации нулем.
ivaigult
Ты прав. Спасибо. Я должен уточнить, остальные параметры будут автоматически инициализированы по умолчанию. Я хотел сказать, что любой, кто надеялся, что это поможет обеспечить полную явную инициализацию типов POD, будет разочарован.
Catskul
43

Поскольку style Aэто запрещено в C ++, и вы не хотите, style Bкак насчет использования style BX:

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

По крайней мере, хоть немного поможет.

iammilind
источник
8
+1: на самом деле это не гарантирует правильную инициализацию (от точки обзора компилятора), но, безусловно, помогает читателю ... хотя комментарии должны быть синхронизированы.
Matthieu M.
18
Комментарий не предотвращает сбой инициализации структуры, если я вставляю новое поле между fooи barв будущем. C по-прежнему будет инициализировать нужные поля, а C ++ - нет. И в этом суть вопроса - как добиться такого же результата на C ++. Я имею в виду, Python делает это с именованными аргументами, C - с именованными полями, и я надеюсь, что в C ++ должно что-то быть.
dmitry_romanov
2
Комментарии синхронизируются? Дай мне перерыв. Безопасность проходит через окно. Измените порядок параметров и стрелы. Намного лучше с explicit FooBar::FooBar(int foo, float bar) : foo(foo), bar(bar) . Обратите внимание на явное ключевое слово. Даже нарушение стандарта лучше с точки зрения безопасности. In Clang: -Wno-c99-extensions
Daniel O
@DanielW, дело не в том, что лучше, а что нет. этот ответ в соответствии с тем, что OP не хочет стиль A (не c ++), B или C, который охватывает все допустимые случаи.
iammilind
@iammilind Я думаю, намек на то, почему ментальная парадигма OP плох, может улучшить ответ. Я считаю это опасным и сейчас.
Daerst
11

Вы можете использовать лямбду:

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

Более подробную информацию об этой идиоме можно найти в блоге Херба Саттера .

ресница
источник
1
Такой подход дважды инициализирует поля. Однажды в конструкторе. Во-вторых fb.XXX = YYY.
Дмитрий Овдиенко
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 ++ достижимо то, чего вы хотите»: разве OP не просил помочь предотвратить перепутывание порядка параметров? Каким образом предлагаемый вами шаблон сможет этого добиться? Для простоты предположим, что у нас есть 2 параметра, оба целые.
максимум
@max: это предотвратит это, только если типы различаются (даже если они могут быть преобразованы друг в друга), что является ситуацией OP. Если он не может различать типы, то он, конечно, не работает, но это совсем другой вопрос.
Matthieu M.
Ах, понял. Да, это две разные проблемы, и я полагаю, что у второй из них на данный момент нет хорошего решения в C ++ (но похоже, что C ++ 20 добавляет поддержку имен параметров в стиле C99 при агрегированной инициализации).
максимум
8

Извлеките конанты в функции, которые их описывают (базовый рефакторинг):

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

Я знаю, что этот стиль очень близок к тому, который вы не хотели использовать, но он позволяет упростить замену постоянных значений, а также объяснить их (таким образом, не нужно редактировать комментарии), если они когда-либо изменятся.

Еще одна вещь, которую вы могли бы сделать (поскольку вы ленивы), - это сделать конструктор встроенным, чтобы вам не приходилось много печатать (удаление «Foobar ::» и время, затрачиваемое на переключение между h и cpp файлом):

struct FooBar {
  FooBar(int f, float b) : foo(f), bar(b) {}
  int foo;
  float bar;
};
ralphtheninja
источник
1
Я настоятельно рекомендую всем, кто прочитает этот вопрос, выбрать стиль в нижнем фрагменте кода для этого ответа, если все, что вы хотите сделать, это иметь возможность быстро инициализировать структуры с набором значений.
kayleeFrye_on, декабрь
6

Многие внешние интерфейсы C ++ компиляторов (включая GCC и clang) понимают синтаксис инициализатора C. Если можете, просто воспользуйтесь этим методом.

Маттиас Урликс
источник
16
Что не соответствует стандарту C ++!
bitmask
5
Я знаю, что это нестандартно. Но если вы можете его использовать, это по-прежнему самый разумный способ инициализировать структуру.
Matthias Urlichs
2
Вы можете защитить типы x и y, сделав неправильный конструктор закрытым:private: FooBar(float x, int y) {};
dmitry_romanov
4
clang (компилятор C ++ на основе llvm) также поддерживает этот синтаксис. Жаль, что это не часть стандарта.
nimrodm 05
Все мы знаем, что инициализаторы C не являются частью стандарта C ++. Но многие компиляторы понимают это, и вопрос не говорит о том, какой компилятор нацелен, если таковой имеется. Поэтому, пожалуйста, не понижайте этот ответ.
Matthias Urlichs
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);
Parapura Rajkumar
источник
2
Громоздко для функционального программирования (т. Е. Создания объекта в списке аргументов вызова функции), но в противном случае - отличная идея!
bitmask
27
оптимизатор, вероятно, уменьшает его, но мои глаза - нет.
Matthieu M.
6
Два слова: ах ... ах! Чем это лучше, чем использование общедоступных данных с 'Point pt; pt.x = pt.y = 20; `? Или, если вам нужна инкапсуляция, чем это лучше, чем конструктор?
OldPeculier
3
Это лучше, чем конструктор, потому что вы должны смотреть на объявление конструктора для порядка параметров ... это x, y или y, x, но способ, который я показал, очевидно на сайте вызова
parapura rajkumar
2
Это не работает, если вам нужна константная структура. или если вы хотите запретить компилятору разрешать неинициализированные структуры. Если вы действительно хотите сделать это так, по крайней мере, отметьте сеттеры inline!
Матиас Урлихс,
3

Вариант D:

FooBar FooBarMake(int foo, float bar)

Юридический C, юридический C ++. Легко оптимизируется для POD. Конечно, именованных аргументов нет, но это как и во всем C ++. Если вам нужны именованные аргументы, лучше выбрать Objective C.

Вариант E:

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

Юридический C, юридический C ++. Именованные аргументы.

Джон
источник
12
Вместо memset, который вы можете использовать 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 , но это может запутать кого-то, кто с ней не знаком.

Переупорядочивание элементов структуры похоже на переупорядочивание параметров функции, такой рефакторинг может вызвать проблемы, если вы не сделаете его очень осторожно.

Öö Tiib
источник
7
Я называю это плохим стилем, потому что считаю, что он не обслуживается. Что, если я добавлю еще одного участника через год? Или если я изменю порядок / типы участников? Каждый фрагмент кода, инициализирующий его, может (очень вероятно) сломаться.
bitmask
2
@bitmask Но пока у вас нет именованных аргументов, вам также придется обновлять вызовы конструкторов, и я думаю, что не многие люди думают, что конструкторы - это неподдерживаемый плохой стиль. Я также думаю, что именованная инициализация - это не C, а C99, из которых C ++ определенно не является надмножеством.
Christian Rau
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. Работает нормально.
Вы также можете создать конкретный макрос, если хотите избежать дублирования имени переменной.

cls
источник
clang++3.5.0-10, -Weverything -std=c++1zпохоже, подтверждает это. Но это не выглядит правильным. Вы знаете, где стандарт подтверждает, что это действительный C ++?
bitmask
Не знаю, но я давно использовал это в разных компиляторах и не видел никаких проблем. Сейчас протестировал на 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);

Этот макрос создает атрибут и метод ссылки на себя.

Эмануэле Паванелло
источник
0

Для версий C ++ до C ++ 20 (которые вводят именованную инициализацию, что делает ваш вариант A действительным в C ++), примите во внимание следующее:

int main()
{
    struct TFoo { int val; };
    struct TBar { float val; };

    struct FooBar {
        TFoo foo;
        TBar bar;
    };

    FooBar mystruct = { TFoo{12}, TBar{3.4} };

    std::cout << "foo = " << mystruct.foo.val << " bar = " << mystruct.bar.val << std::endl;

}

Обратите внимание: если вы попытаетесь инициализировать структуру с помощью FooBar mystruct = { TFoo{12}, TFoo{3.4} }; вы получите ошибку компиляции.

Обратной стороной является то, что вам нужно создать одну дополнительную структуру для каждой переменной внутри вашей основной структуры, а также вы должны использовать внутреннее значение с mystruct.foo.val. Но с другой стороны, он чистый, простой, чистый и стандартный.

Нельсон
источник