Почему инициализация списка (с использованием фигурных скобок) лучше, чем альтернативы?

406
MyClass a1 {a};     // clearer and less error-prone than the other three
MyClass a2 = {a};
MyClass a3 = a;
MyClass a4(a);

Почему?

Я не смог найти ответ на SO, поэтому позвольте мне ответить на мой собственный вопрос.

Алексей
источник
12
Почему бы не использовать auto?
Марк Гарсия
33
Это правда, это удобно, но, на мой взгляд, снижает читабельность - мне нравится видеть, какой тип объекта при чтении кода. Если вы на 100% уверены, какой тип объекта, зачем использовать auto? И если вы используете инициализацию списка (прочитайте мой ответ), вы можете быть уверены, что это всегда правильно.
Алексей
103
@Oleksiy: std::map<std::string, std::vector<std::string>>::const_iteratorхотел бы поговорить с вами.
Xeo
9
@Oleksiy Я рекомендую прочитать это GotW .
Раппц
17
@doc Я бы сказал, что using MyContainer = std::map<std::string, std::vector<std::string>>;это даже лучше (особенно если вы можете создать шаблон!)
JAB

Ответы:

357

В основном, копирование и вставка из Бьярна Страуструпа "Язык программирования C ++, 4-е издание" :

Инициализация списка не позволяет сужаться (§iso.8.5.4). Это:

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

Пример:

void fun(double val, int val2) {

    int x2 = val; // if val==7.9, x2 becomes 7 (bad)

    char c2 = val2; // if val2==1025, c2 becomes 1 (bad)

    int x3 {val}; // error: possible truncation (good)

    char c3 {val2}; // error: possible narrowing (good)

    char c4 {24}; // OK: 24 can be represented exactly as a char (good)

    char c5 {264}; // error (assuming 8-bit chars): 264 cannot be 
                   // represented as a char (good)

    int x4 {2.0}; // error: no double to int value conversion (good)

}

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

Пример:

auto z1 {99};   // z1 is an int
auto z2 = {99}; // z2 is std::initializer_list<int>
auto z3 = 99;   // z3 is an int

Вывод

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

Алексей
источник
52
Существует также тот факт, что использование ()может быть проанализировано как объявление функции. Это сбивает с толку и противоречиво, что вы можете сказать, T t(x,y,z);но нет T t(). И иногда, вы уверены x, вы даже не можете сказать T t(x);.
Juanchopanza
84
Я категорически не согласен с этим ответом; инициализация в скобках превращается в полный беспорядок, когда у вас есть типы с ctor, принимающим a std::initializer_list. RedXIII упоминает эту проблему (и просто отмахивается от нее), тогда как вы полностью игнорируете ее. A(5,4)и A{5,4}может вызывать совершенно разные функции, и это важно знать. Это может даже привести к вызовам, которые кажутся неинтуитивными. Сказать, что вы предпочитаете {}по умолчанию, приведет к тому, что люди не поймут, что происходит. Это не твоя вина, хотя. Я лично считаю, что это крайне плохо продуманная функция.
user1520427
13
@ user1520427 Вот почему есть часть " если у вас нет веских причин не делать этого ".
Алексей
67
Хотя этот вопрос старый, у него довольно много посещений, поэтому я добавляю его сюда только для справки (я не видел его нигде на странице). Из C ++ 14 с новыми Правилами для автоматического вывода из braced-init-list теперь можно писать, auto var{ 5 }и оно будет выведено как intне более, чем std::initializer_list<int>.
Эдоардо Спаркон Доминик
13
Ха-ха, из всех комментариев до сих пор не ясно, что делать. Ясно, что спецификация C ++ - беспорядок!
DrumM
113

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

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

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

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

vector<int> a{10,20};   //Curly braces -> fills the vector with the arguments

vector<int> b(10,20);   //Parentheses -> uses arguments to parametrize some functionality,                          
vector<int> c(it1,it2); //like filling the vector with 10 integers or copying a range.

vector<int> d{};      //empty braces -> default constructs vector, which is equivalent
                      //to a vector that is filled with zero elements
MikeMB
источник
11
Полностью согласен с большинством ваших ответов. Однако не думаете ли вы, что пустые скобки для вектора просто излишни? Я имею в виду, это нормально, когда вам нужно инициализировать значение объекта универсального типа T, но какова цель сделать это для неуниверсального кода?
Михаил
8
@Mikhail: Это, конечно, избыточно, но моя привычка всегда делать явную инициализацию локальной переменной. Как я уже писал, речь идет главным образом о случайности, поэтому я не забываю это, когда это имеет значение. Это, конечно, ничего, что я бы упомянул в обзоре кода или добавить в руководство по стилю.
MikeMB
4
довольно чистый набор правил.
laike9m
5
Это, безусловно, лучший ответ. {} - это как наследование - легко злоупотреблять, что приводит к сложному для понимания коду.
UKMonkey
2
Пример @MikeMB: const int &b{}<- не пытается создать неинициализированную ссылку, а связывает ее с временным целочисленным объектом. Второй пример: struct A { const int &b; A():b{} {} };<- не пытается создать неинициализированную ссылку (как ()было бы), но связывает ее с временным целочисленным объектом, а затем оставляет его висящим. GCC даже с -Wallне предупреждает для второго примера.
Йоханнес Шауб -
92

Есть много причин для инициализации использования распорки, но вы должны знать , что конструктор предпочтительнее других конструкторы , исключение составляет по умолчанию, конструктор. Это приводит к проблемам с конструкторами и шаблонами, где конструктор типа может быть либо списком инициализатора, либо простым старым ctor.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); // copy ctor
    Foo c{a}; // copy ctor (init. list element) + initializer list!!!
}

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

Красный XIII
источник
20
Это очень важный момент в общем программировании. Когда вы пишете шаблоны, не используйте braced-init-lists (имя стандарта для { ... }), если вам не нужна initializer_listсемантика (ну, и, возможно, для создания объекта по умолчанию).
Xeo
82
Я, честно говоря, не понимаю, почему std::initializer_listправило существует - оно просто добавляет путаницы и беспорядка в язык. Что не так делать, Foo{{a}}если вы хотите std::initializer_listконструктор? Это кажется гораздо проще понять, чем иметь std::initializer_listприоритет над всеми другими перегрузками.
user1520427
5
+1 за комментарий выше, потому что это действительно беспорядок, я думаю! это не логика; Foo{{a}}следует некоторой логике для меня гораздо больше, чем то, Foo{a}что превращается в приоритет списка инициализаторов (ups может подумать пользователь hm ...)
Габриэль
32
В основном C ++ 11 заменяет один беспорядок другим беспорядком. Ой, извините, это не заменяет его - оно добавляет к нему. Как вы можете узнать, если вы не сталкивались с такими классами? Что если вы начнете без std::initializer_list<Foo> конструктора, но в какой-то момент он будет добавлен в Fooкласс для расширения его интерфейса? Тогда пользователи Fooкласса облажались.
док
10
.. каковы "МНОГИЕ причины для использования инициализации скобки"? В этом ответе указывается одна из причин ( initializer_list<>), которая на самом деле не определяет, кто предпочитает, а затем приводит один хороший случай, когда он НЕ предпочтителен. Что мне не хватает, что ~ 30 других людей (по состоянию на 2016-04-21) сочли полезным?
Двандерсон
0

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

Примечание: A) фигурные скобки безопаснее, поскольку они не допускают сужения. Б) Вьющиеся скобки менее безопасны, потому что они могут обходить частные или удаленные конструкторы и неявно вызывать явно помеченные конструкторы.

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

Аллан Дженсен
источник
Я попытался покопаться в goldbolt.org, чтобы обойти «явные» или «частные» конструкторы, используя предоставленный пример кода и сделав один или другой частным или явным, и был вознагражден соответствующей ошибкой компилятора [s]. Хотите подкрепить это некоторым примером кода?
Марк Сторер
Это исправление для проблемы, предложенной для C ++ 20: open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf
Аллан Дженсен
1
Если вы отредактируете свой ответ, чтобы показать версию C ++, о которой вы говорите, я буду рад изменить свой голос.
Марк Сторер