Чи можна запобігти пропущенню сукупності членів ініціалізації?


43

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

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

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

VariablePointers{activePtr, filename}

Компілятор не буде скаржитися на це, але у мене буде один об’єкт, який частково ініціалізований. Як я можу запобігти подібні помилки? Я міг би додати конструктор, але він би повторював перелік змінної двічі, тому мені потрібно вводити все це тричі!

Будь ласка, додайте відповіді на C ++ 11 , якщо є рішення для C ++ 11 (наразі я обмежений цією версією). Однак новіші мовні стандарти теж вітаються!


6
Введення конструктора виглядає не так страшно. Якщо у вас занадто багато членів, то, можливо, може бути рефакторинг в порядку.
Gonen I

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), але має працювати на будь-якій розумній платформі.

Ви можете видалити оператор перетворення, і тоді це помилка компілятора.
jrok

@jrok так, але це один, як тільки Fooце оголошено, навіть якщо ви ніколи насправді не дзвоните оператору.
Квентін

2
@jrok Але він не компілюється, навіть якщо ініціалізація передбачена. godbolt.org/z/yHZNq_ Додаток: Для MSVC він працює так, як ви описали: godbolt.org/z/uQSvDa Це помилка?
n314159

Звичайно, нерозумно мене.
jrok

6
На жаль, ця хитрість не працює з C ++ 11, оскільки вона стане неагрегаторною :( Я видалив тег C ++ 11, тож відповідь ваша життєздатна (будь ласка, не видаляйте її), але рішення С ++ 11, якщо можливо, все ще є кращим.
Йоханнес Шауб -

22

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

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


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, у прикладі) або ви отримуєте помилку "виклик видаленому конструктору" бар ".

Тому

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

складати і

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

не робить.

На жаль також

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

не компілюється.

- EDIT -

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

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

і це може бути дуже заплутано.

Щоб уникнути цієї проблеми, я додав наступний видалений конструктор шаблонів

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

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


Дякую, це приємно! Як ви вже згадували, це не ідеально, а також foo f;не дає змоги скласти, але, можливо, це скоріше функція, ніж недолік з цим трюком. Прийму, якщо немає кращої пропозиції, ніж ця.
Йоханнес Шауб - ліб

1
Я б змусив конструктор барів прийняти const вкладений член класу, який називається щось на зразок init_list_end для читабельності
Gonen, я

@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частина просто тривіальний приклад , щоб показати , що рішення працює; точний "тип без ініціалізації за замовчуванням" повинен залежати від обставин (IMHO).
max66

4

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

Оновлення:

Правило, яке потрібно перевірити, є частиною типу безпеки Type.6:

Тип 6: Завжди ініціалізуйте змінну члена: завжди ініціалізуйте, можливо, використовуючи конструктори за замовчуванням або ініціалізатори членів за замовчуванням.


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 

Інший варіант: Якщо ваші члени const &, ви повинні ініціалізувати їх усіх:

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

Якщо ви можете жити з одним манекеном const & member, ви можете поєднати це з ідеєю @ 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вже не є сукупністю значень, так би мовити) :(
Йоханнес Шауб - ліб

@ JohannesSchaub-litb OK. Як щодо цієї ідеї в моїй відредагованій відповіді?
Gonen I

@ JohannesSchaub-litb: не менш важливо, що перша версія додає рівня опосередкованості, роблячи покажчики членів. Ще важливіше, що вони повинні бути посиланням на щось, а 1,2,3об'єкти - це фактично локальні локатори в автоматичному сховищі, які виходять із сфери застосування, коли функція закінчується. І це робить розмір (A) 24 замість 3 у системі з 64-бітовими вказівниками (як x86-64).
Пітер Кордес

Макетна посилання збільшує розмір від 3 до 16 байт (накладка для вирівнювання елемента вказівника (посилання) + сам вказівник.) Якщо ви ніколи не використовуєте посилання, це, ймовірно, добре, якщо він вказує на об'єкт, який вийшов із сфера застосування. Я, безумовно, переживаю, щоб це не було оптимізовано, і копіювання навколо цього точно не буде. (Порожній клас має більше шансів оптимізувати його, ніж його розмір, тому третій варіант тут є найменш поганим, але він все одно коштує місця в кожному об'єкті, принаймні, в деяких ABI. Я б ще не турбувався про те, що шкода завдає шкоди оптимізація в деяких випадках.)
Пітер Кордес,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.