Удобная инициализация структуры 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


Caveat emptor: имейте в виду, что если вы добавите параметры в конец структуры позже, старые инициализации все равно будут компилироваться без уведомления без инициализации.
— Catskul

1
@Catskul Нет. Он будет инициализирован пустым списком инициализаторов, что приведет к инициализации нулем.
— ivaigult

Ты прав. Спасибо. Я должен уточнить, остальные параметры будут автоматически инициализированы по умолчанию. Я хотел сказать, что любой, кто надеялся, что это поможет обеспечить полную явную инициализацию типов POD, будет разочарован.
— Catskul

43

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

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

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


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

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

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 , но это может запутать кого-то, кто с ней не знаком.

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


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. Работает нормально.
Вы также можете создать конкретный макрос, если хотите избежать дублирования имени переменной.


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. Но с другой стороны, он чистый, простой, чистый и стандартный.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.