6

Каковы преимущества инициализации списка (с использованием фигурных скобок)?

6

Описание проблемы:

Я столкнулся с вопросом о том, как правильно и эффективно инициализировать объекты класса в C++. У меня есть следующий код:

MyClass a1 {a};     // более очевидно и менее подвержено ошибкам по сравнению с остальными тремя
MyClass a2 = {a};
MyClass a3 = a;
MyClass a4(a);

Вопрос:

Почему вариант MyClass a1 {a}; считается более ясным и менее подверженным ошибкам, чем остальные способы инициализации (a2, a3 и a4)? Какие преимущества есть у списка инициализации (uniform initialization) по сравнению с другими подходами?

4 ответ(ов)

5

Список инициализации не позволяет сужающего преобразования (§iso.8.5.4). Это означает, что:

  • Целое число не может быть преобразовано в другое целое число, которое не может хранить его значение. Например, преобразование char в int допустимо, но не допускается преобразование int в char.
  • Значение с плавающей запятой не может быть преобразовано в другой тип с плавающей запятой, который не может хранить его значение. Например, преобразование float в double допустимо, но не допускается преобразование double в float.
  • Значение с плавающей запятой не может быть преобразовано в целочисленный тип.
  • Целочисленное значение не может быть преобразовано в тип с плавающей запятой.

Пример:

void fun(double val, int val2) {

    int x2 = val;    // если val == 7.9, x2 становится 7 (плохо)

    char c2 = val2;  // если val2 == 1025, c2 становится 1 (плохо)

    int x3 {val};    // ошибка: возможное усечение (хорошо)

    char c3 {val2};  // ошибка: возможное сужение (хорошо)

    char c4 {24};    // ОК: 24 может быть точно представлено как char (хорошо)

    char c5 {264};   // ошибка (предполагается, что `char` 8-битный): 264 нельзя 
                     // представить как char (хорошо)

    int x4 {2.0};    // ошибка: нет преобразования значения double в int (хорошо)

}

Единственная ситуация, когда предпочтительнее использовать = вместо {} — это при использовании ключевого слова auto, чтобы получить тип, определяемый инициализатором.

Пример:

auto z1 {99};   // z1 является int
auto z2 = {99}; // z2 является std::initializer_list<int>
auto z3 = 99;   // z3 является int

Заключение

Предпочитайте инициализацию с помощью над альтернативами, если у вас нет веской причины не делать этого.

2

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

  • Если создаваемый объект концептуально содержит значения, которые я передаю в конструктор (например, контейнеры, POD-структуры, атомарные типы, умные указатели и т.д.), то я использую фигурные скобки.
  • Если конструктор напоминает обычный вызов функции (он выполняет более или менее сложные операции, параметризованные аргументами), тогда я применяю обычный синтаксис вызова функции.
  • Для инициализации по умолчанию я всегда использую фигурные скобки.

Таким образом, я всегда уверен, что объект инициализируется, независимо от того, является ли он "реальным" классом с конструктором по умолчанию, который все равно будет вызван, или встроенным / POD-типом. Во-вторых, это — в большинстве случаев — согласуется с первым правилом, так как инициализированный по умолчанию объект часто представляет собой "пустой" объект.

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

Например, это хорошо сочетается с типами стандартной библиотеки, такими как std::vector:

vector<int> a{10, 20};   // Фигурные скобки -> заполняет вектор переданными аргументами

vector<int> b(10, 20);   // Скобки -> использует аргументы для параметризации некоторой функциональности,
vector<int> c(it1, it2); // например, заполняя вектор 10 целыми числами или копируя диапазон.

vector<int> d{};         // Пустые фигурные скобки -> стандартная инициализация вектора,
                         // что эквивалентно вектору, заполненному нулевыми элементами.
1

Существует множество причин для использования инициализации через фигурные скобки, но следует учитывать, что конструктор с initializer_list<> предпочтителен по сравнению с другими конструкторами, за исключением конструктора по умолчанию. Это может привести к проблемам с конструкторами и шаблонами, где конструктор типа T может быть как инициализатором списка, так и обычным конструктором.

struct Foo {
    Foo() {}

    Foo(std::initializer_list<Foo>) {
        std::cout << "initializer list" << std::endl;
    }

    Foo(const Foo&) {
        std::cout << "copy ctor" << std::endl;
    }
};

int main() {
    Foo a;
    Foo b(a); // копирующий конструктор
    Foo c{a}; // копирующий конструктор (элемент инициализирующего списка) + инициализация через список!!!
}

Если вы не сталкиваетесь с подобными классами, то нет особых причин не использовать инициализатор списка.

0

Это безопаснее только до тех пор, пока вы не используете флаг -Wno-narrowing, как это делает, например, Google в Chromium. Если вы используете этот флаг, то это становится МЕНее безопасным. Без этого флага только небезопасные случаи будут исправлены в C++20.

Важно отметить:

  1. Фигурные скобки (curly brackets) более безопасны, так как не позволяют сужение типов (narrowing).
  2. Фигурные скобки менее безопасны, потому что могут обойти приватные или удаленные конструкторы, а также неявно вызвать конструкторы, помеченные как explicit.

В совокупности это означает, что фигурные скобки более безопасны, если внутри находятся примитивные константы, но менее безопасны, если это объекты (хотя это будет исправлено в C++20).

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь