Можно ли предотвратить пропуск элементов инициализации агрегата?

43

У меня есть структура со многими членами одного типа, как это

struct VariablePointers {
   VariablePtr active;
   VariablePtr wasactive;
   VariablePtr filename;
};

Проблема в том, что если я забуду инициализировать один из членов структуры (например wasactive), например:

VariablePointers{activePtr, filename}

Компилятор не будет жаловаться на это, но у меня будет один объект, который частично инициализирован. Как я могу предотвратить подобные ошибки? Я мог бы добавить конструктор, но он дважды продублировал бы список переменных, так что мне пришлось бы напечатать все это трижды!

Пожалуйста, также добавьте ответы C ++ 11 , если есть решение для C ++ 11 (в настоящее время я ограничен этой версией). Более поздние языковые стандарты также приветствуются!

Йоханнес Шауб - Литб
источник
6
Ввод конструктора звучит не так ужасно. Если у вас не слишком много участников, в этом случае, возможно, рефакторинг в порядке.
Гонен, я
1
@ Someprogrammerdude Я думаю, он имеет в виду, что ошибка в том, что вы можете случайно пропустить инициализирующее значение
Gonen I
2
@theWiseBro, если вы знаете, как массив / вектор помогает, вы должны опубликовать ответ. Это не так очевидно, я не вижу этого
idclev 463035818
2
@ Someprogrammerdude Но это даже предупреждение? Не могу видеть это с VS2019.
acraig5075
8
Есть -Wmissing-field-initializersфлаг компиляции.
Рон

Ответы:

42

Вот трюк, который вызывает ошибку компоновщика, если отсутствует необходимый инициализатор:

struct init_required_t {
    template <class T>
    operator T() const; // Left undefined
} static const init_required;

Применение:

struct Foo {
    int bar = init_required;
};

int main() {
    Foo f;
}

Результат:

/tmp/ccxwN7Pn.o: In function `Foo::Foo()':
prog.cc:(.text._ZN3FooC2Ev[_ZN3FooC5Ev]+0x12): undefined reference to `init_required_t::operator int<int>() const'
collect2: error: ld returned 1 exit status

Предостережения:

  • До C ++ 14 это Fooвообще не позволяет быть агрегатом.
  • Технически это зависит от неопределенного поведения (нарушение ODR), но должно работать на любой разумной платформе.
Quentin
источник
Вы можете удалить оператор преобразования, и тогда это ошибка компилятора.
Jrok
@jrok да, но он один, как только Fooобъявлен, даже если вы никогда не вызываете оператора.
Квентин
2
@jrok Но тогда он не компилируется, даже если инициализация обеспечена. godbolt.org/z/yHZNq_ Приложение: Для MSVC это работает так, как вы описали: godbolt.org/z/uQSvDa Это ошибка?
n314159
Конечно, глупый я.
Jrok
6
К сожалению, этот трюк не работает с C ++ 11, так как тогда он станет неагрегированным :( Я удалил тег C ++ 11, поэтому ваш ответ также является жизнеспособным (пожалуйста, не удаляйте его), но решение C ++ 11 по-прежнему предпочтительнее, если это возможно
Йоханнес Шауб - litb
22

Для clang и gcc вы можете скомпилировать, -Werror=missing-field-initializersчто превращает предупреждение об отсутствующих инициализаторах поля в ошибку. godbolt

Редактировать: Для MSVC, кажется, нет предупреждения даже на уровне /Wall, поэтому я не думаю, что с помощью этого компилятора можно предупредить об отсутствующих инициализаторах. godbolt

n314159
источник
7

Полагаю, это не элегантное и удобное решение, но должно работать и с C ++ 11 и выдавать ошибку во время компиляции (не во время компоновки).

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

Примером

struct bar
 {
   bar () = delete;

   template <typename T> 
   bar (T const &) = delete;

   bar (int) 
    { }
 };

struct foo
 {
   char a;
   char b;
   char c;

   bar sentinel;
 };

Таким образом, вы вынуждены добавить все элементы в ваш список инициализации агрегата, включая значение для явной инициализации последнего значения ( sentinelв примере это целое число ) или вы получаете ошибку «вызов удаленного конструктора« bar »».

Так

foo f1 {'a', 'b', 'c', 1};

компилировать и

foo f2 {'a', 'b'};  // ERROR

не делает.

К сожалению также

foo f3 {'a', 'b', 'c'};  // ERROR

не компилируется

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

Как указал MSalters (спасибо), в моем исходном примере есть дефект (еще один дефект): barзначение может быть инициализировано charзначением (которое можно преобразовать в int), поэтому работает следующая инициализация

foo f4 {'a', 'b', 'c', 'd'};

и это может быть очень запутанным.

Чтобы избежать этой проблемы, я добавил следующий удаленный конструктор шаблона

 template <typename T> 
 bar (T const &) = delete;

поэтому предыдущее f4объявление дает ошибку компиляции, потому что dзначение перехватывается удаляемым конструктором шаблона

max66
источник
Спасибо, это мило! Он не идеален, как вы упомянули, а также foo f;не может скомпилироваться, но, возможно, это скорее особенность, чем недостаток этого трюка. Приму, если нет лучшего предложения, чем это.
Йоханнес Шауб -
1
Я бы сделал так, чтобы конструктор bar принимал константный вложенный член класса, называемый чем-то вроде init_list_end, для удобства чтения
Gonen I
@GonenI - для удобства чтения вы можете принять enum, и назвать init_list_end(просто list_end) значение этого enum; но удобочитаемость добавляет много машинописи, поэтому, учитывая, что дополнительное значение является слабым местом этого ответа, я не знаю, хорошая ли это идея.
max66
Может быть, добавить что-то вроде constexpr static int eol = 0;в шапке bar. test{a, b, c, eol}кажется довольно читабельным для меня.
n314159
@ n314159 - ну ... стать bar::eol; это почти как передать enumзначение; но я не думаю, что это важно: суть ответа - «добавьте в свою структуру дополнительный член, в последней позиции, типа без инициализации по умолчанию»; эта barчасть - просто тривиальный пример, показывающий, что решение работает; точный «тип без инициализации по умолчанию» должен зависеть от обстоятельств (ИМХО).
max66
4

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

Обновить:

Правило, которое вы хотите проверить, является частью безопасности типов Type.6:

Тип.6: Всегда инициализировать переменную-член: всегда инициализировать, возможно, используя конструкторы по умолчанию или инициализаторы членов по умолчанию.

darune
источник
2

Самый простой способ - не давать типу членов конструктор без аргументов:

struct B
{
    B(int x) {}
};
struct A
{
    B a;
    B b;
    B c;
};

int main() {

        // A a1{ 1, 2 }; // will not compile 
        A a1{ 1, 2, 3 }; // will compile 

Другой вариант: если ваши члены являются постоянными, вы должны инициализировать их всех:

struct A {    const int& x;    const int& y;    const int& z; };

int main() {

//A a1{ 1,2 };  // will not compile 
A a2{ 1,2, 3 }; // compiles OK

Если вы можете жить с одним фиктивным константой и участником, вы можете объединить это с идеей @ max66 о страже.

struct end_of_init_list {};

struct A {
    int x;
    int y;
    int z;
    const end_of_init_list& dummy;
};

    int main() {

    //A a1{ 1,2 };  // will not compile
    //A a2{ 1,2, 3 }; // will not compile
    A a3{ 1,2, 3,end_of_init_list() }; // will compile

Из cppreference https://en.cppreference.com/w/cpp/language/aggregate_initialization

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

Другой вариант - взять сторожевую идею max66 и добавить синтаксический сахар для удобства чтения.

struct init_list_guard
{
    struct ender {

    } static const end;
    init_list_guard() = delete;

    init_list_guard(ender e){ }
};

struct A
{
    char a;
    char b;
    char c;

    init_list_guard guard;
};

int main() {
   // A a1{ 1, 2 }; // will not compile 
   // A a2{ 1, init_list_guard::end }; // will not compile 
   A a3{ 1,2,3,init_list_guard::end }; // compiles OK
Гонен я
источник
К сожалению, это делает Aнеподвижным и изменяет семантику копирования ( Aтак сказать, больше не является совокупностью значений) :(
Йоханнес Шауб - litb
@ JohannesSchaub-litb ОК. Как насчет этой идеи в моем отредактированном ответе?
Гонен я
@ JohannesSchaub-litb: не менее важно, что первая версия добавляет уровень косвенности, делая указатели членов. Что еще более важно, они должны быть ссылкой на что-то, а 1,2,3объекты являются автоматически локальными в автоматическом хранилище, которые выходят из области видимости после завершения функции. И это делает размер (A) 24 вместо 3 в системе с 64-битными указателями (например, x86-64).
Питер Кордес
Фиктивная ссылка увеличивает размер с 3 до 16 байтов (заполнение для выравнивания элемента указателя (ссылки) + самого указателя.) Если ссылка никогда не используется, то, вероятно, это нормально, если она указывает на объект, который вышел из объем. Я бы, конечно, беспокоился о том, чтобы это не оптимизировалось, а копировать его, конечно же, не буду. (Пустой класс имеет больше шансов на оптимизацию, чем его размер, поэтому третий вариант здесь наименее плох, но он все равно стоит места в каждом объекте, по крайней мере, в некоторых ABI. Я бы все еще беспокоился о вреде заполнения оптимизация в некоторых случаях.)
Питер Кордес