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

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

Почему?


person Oleksiy    schedule 14.08.2013    source источник
comment
Почему бы не использовать _1 _?   -  person Mark Garcia    schedule 14.08.2013
comment
@MarkGarcia А? Пожалуйста, дополните :)   -  person Oleksiy    schedule 14.08.2013
comment
Не поместится в поле для комментариев;). В любом случае, если процитировать статью по ссылке: ... основные причины для объявления переменных с использованием auto - это правильность, производительность, ремонтопригодность и надежность - и, да, удобство ....   -  person Mark Garcia    schedule 14.08.2013
comment
Это правда, это удобно, но, на мой взгляд, это снижает удобочитаемость - мне нравится видеть тип объекта при чтении кода. Если вы на 100% уверены, к какому типу относится объект, зачем использовать auto? И если вы используете инициализацию списка (читайте мой ответ), можете быть уверены, что она всегда правильная.   -  person Oleksiy    schedule 14.08.2013
comment
@Oleksiy: std::map<std::string, std::vector<std::string>>::const_iterator хотел бы поговорить с вами.   -  person Xeo    schedule 14.08.2013
comment
@Oleksiy Я рекомендую прочитать этот GotW.   -  person Rapptz    schedule 14.08.2013
comment
@MSalters: Эх, это уже можно делать без auto. Конечно, это мог быть просто забавный укол, который не следует останавливать фактами. ;)   -  person Xeo    schedule 14.08.2013
comment
@Xeo typedef std::map<std::string, std::vector<std::string>> MyContainer обычно более чистая альтернатива. Я использую auto только для типов локальной области видимости.   -  person doc    schedule 29.05.2015
comment
@doc Я бы сказал, что using MyContainer = std::map<std::string, std::vector<std::string>>; даже лучше (тем более, что вы можете его использовать по шаблону!)   -  person JAB    schedule 19.02.2016
comment
Первый и второй не совсем эквивалентны. Первый - это инициализация прямого списка, второй - инициализация списка копирования. См. en.cppreference.com/w/cpp/language/list_initialization.   -  person libertylocked    schedule 04.05.2016
comment
Возможный дубликат: stackoverflow.com/q/9976927/3560202?   -  person Post Self    schedule 22.03.2017
comment
Также см. isocpp.github.io/CppCoreGuidelines/.   -  person usernameiwantedwasalreadytaken    schedule 10.01.2018
comment
Просто последнее добавление: почти всегда auto делает Святой Грааль - а это не !!!. Есть веские причины для использования auto (пример итератора выше), но в других ситуациях его лучше не использовать. Примеры:   -  person Aconcagua    schedule 13.08.2018
comment
unsigned int n = 7; vs. auto n = 7U; - суффикс легко забыть (что на самом деле произошло в первом варианте, намеренно, в демонстрационных целях, однако первый вариант является устойчивым), что приводит к n плохого типа. Еще хуже: auto n = 7UL; там, где вы хотите, чтобы n было типа uint64_t - и BAM, мы не на 64-битном Linux и просто получаем вместо этого uint32_t (или, возможно, даже не этот, поскольку uint32_t может быть определен как unsigned int в текущей системе, а вы может закончиться, например, несовместимыми указателями).   -  person Aconcagua    schedule 13.08.2018
comment
Боже, только в C ++ такие вопросы вообще существуют. Большое спасибо за вопрос, ответы действительно помогли   -  person CoffeeTableEspresso    schedule 15.04.2019
comment
@CoffeeTableEspresso, ура, брат! Надеюсь, скоро будет опубликовано Руководство Apocryph по теологии инициализации C ++.   -  person Sz.    schedule 09.08.2019


Ответы (4)


В основном копирование и вставка из The C ++ Programming Language 4th Edition Бьярна Страуструпа:

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

  • Целое число не может быть преобразовано в другое целое число, которое не может содержать свое значение. Например, допускается преобразование char в int, но не из int в char.
  • Значение с плавающей запятой нельзя преобразовать в другой тип с плавающей запятой, который не может содержать свое значение. Например, можно использовать float для 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

Заключение

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

person Oleksiy    schedule 14.08.2013
comment
Также существует тот факт, что использование () может быть проанализировано как объявление функции. Это сбивает с толку и непоследовательно, что вы можете сказать T t(x,y,z);, но не T t(). А иногда, вы уверены x, вы даже не можете сказать T t(x);. - person juanchopanza; 14.08.2013
comment
Я категорически не согласен с этим ответом; фигурная инициализация превращается в полный беспорядок, когда у вас есть типы с ctor, принимающим std::initializer_list. RedXIII упоминает об этой проблеме (и просто отмахивается от нее), в то время как вы полностью игнорируете ее. A(5,4) и A{5,4} могут вызывать совершенно разные функции, и это важно знать. Это может даже привести к звонкам, которые кажутся не интуитивно понятными. Сказать, что вы должны предпочесть {} по умолчанию, приведет к тому, что люди неправильно поймут, что происходит. Но это не твоя вина. Я лично считаю, что это крайне плохо продуманная функция. - person user1520427; 02.02.2015
comment
@ user1520427 Вот почему есть , если у вас нет веских причин не расставаться. - person Oleksiy; 02.02.2015
comment
@Oleksiy Я бы сказал, что всегда есть веская причина рассмотреть альтернативы, особенно когда простое добавление std::initializer_list конструктора может изменить семантику существующего {} вызова. Честно говоря, я не понимаю, как вы можете прийти к такому выводу, но, может быть, это просто потому, что я думаю, что для этой части языка нужно было приложить больше усилий. Ну что ж. - person user1520427; 03.02.2015
comment
@ user1520427 Я понимаю, что вы говорите, но в этом случае конструктор, принимающий std::initializer_list, является единственной причиной для рассмотрения альтернатив. В всех сценариях предпочтительна {} инициализация. Дай мне знать, если я чего-то не понимаю. - person Oleksiy; 03.02.2015
comment
Хотя этот вопрос старый, он получил довольно много ответов, поэтому я добавляю его здесь только для справки (я не видел его нигде на странице). Начиная с C ++ 14 с новыми Правилами для автоматическое вывод из списка инициализации в скобках теперь можно написать auto var{ 5 }, и он будет выводиться как int, но не как std::initializer_list<int>. - person Edoardo Dominici; 31.10.2015
comment
Пример с авто в настоящее время неверен. Его следует изменить на следующее: z1 - это целое число, z2 - это целое число и auto z3 = {99}; // z3 is an std::initializer_list<int>. - person Georg; 11.12.2016
comment
@juanchopanza, это относится к самой неприятной проблеме синтаксического анализа, верно? Или есть другие ситуации, когда это могло произойти? - person batbrat; 07.03.2017
comment
@batbrat Да, вот и все. - person juanchopanza; 07.03.2017
comment
@Oleksiy Для строк - любая разница в использовании между строкой s1 (Привет); и строка S2 {Hello}; Какой на лучше? - person Rajesh; 18.01.2018
comment
Не думаю, что это объясняет, почему это вообще лучше. Не только загружен вопрос, который вы предполагали, но и ответ просто указывает, как работает инициализационное преобразование типов {}; что не безопаснее, чем преобразования, выполняемые при вызове функций. Прекрасный пример худшего - struct X{int i; int j}; X {5};, который будет компилироваться; но оставьте j неинициализированным - person UKMonkey; 09.05.2018
comment
edit j сконструирован по умолчанию - но если j необходимо заполнить, чтобы класс имел смысл, все пойдет не так. - person UKMonkey; 09.05.2018
comment
@EdoardoSparkonDominici Это изменение не вошло в C ++ 14; потребовалось добавить C ++ 17. - person underscore_d; 19.09.2018
comment
Всегда прыгайте с моста, если у вас нет веских причин не использовать @user, я интерпретирую это так. - person Johannes Schaub - litb; 18.11.2018
comment
Инициализация списка @juanchopanza имеет свои недостатки. Вы можете сказать T &t{otherT}, но не T &t{otherT, somethingElse}, но можете сказать const T& t{otherT} и const T& t{otherT, somethingElse}. Однако в первом случае &t == &otherT, но во втором случае создается временный файл. Инициализация в старом стиле последовательно защищает от этой опасности и просто делает вариант с несколькими аргументами плохо сформированным. - person Johannes Schaub - litb; 18.11.2018
comment
Ха-ха, из всех комментариев до сих пор непонятно, что делать. Ясно одно: спецификация C ++ - беспорядок! - person DrumM; 12.03.2019
comment
авто z1 {99}; это список типа int. См. Нижние примеры Stroustrup C ++ 4-е изд. - person notaorb; 04.06.2020
comment
Я хочу добавить youtube.com/watch?v=3MB2iiCkGxg 35:00 - person Norhther; 26.07.2020
comment
Обратите внимание, что в некоторых примерах с пометкой «ошибка» может появиться только предупреждение компилятора. См. gcc.gnu.org/wiki/FAQ#Wnarrowing. - person Lack; 23.03.2021

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

  • Если объект, который я создаю, концептуально содержит значения, которые я передаю в конструкторе (например, контейнеры, структуры 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
person MikeMB    schedule 14.05.2016
comment
Полностью согласен с большей частью вашего ответа. Однако не кажется ли вам, что пустые фигурные скобки для вектора просто излишни? Я имею в виду, что это нормально, когда вам нужно инициализировать значение объекта общего типа T, но какова цель этого для неуниверсального кода? - person Mikhail; 27.11.2016
comment
@Mikhail: Это, конечно, излишне, но у меня есть привычка всегда делать инициализацию локальной переменной явной. Как я уже писал, в основном речь идет о согласованности, поэтому я не забываю об этом, когда это имеет значение. Это, конечно, не то, что я бы упомянул в обзоре кода или добавил в руководство по стилю. - person MikeMB; 27.11.2016
comment
довольно чистый набор правил. - person laike9m; 20.03.2018
comment
Это, безусловно, лучший ответ. {} похож на наследование - им легко злоупотребить, что приводит к трудному пониманию кода. - person UKMonkey; 09.05.2018
comment
Во-первых, таким образом я всегда уверен, что объект инициализируется ... к сожалению, для ссылок это не так. Но в этом случае фигурные скобки init могут незаметно создавать висячие ссылки на временные библиотеки. - person Johannes Schaub - litb; 18.11.2018
comment
@ JohannesSchaub-litb: Извините, я не совсем понимаю, о чем вы. Как я могу создать единичную ссылку (разве это не должно вызывать ошибку времени компиляции?)? И поскольку эта часть была о создании без каких-либо параметров (да, инициализация по умолчанию, вероятно, не правильный термин для этого, но я отказался от классификации различных типов инициализации), я также не понимаю, как это должно создавать висящую ссылку. - person MikeMB; 18.11.2018
comment
Пример @MikeMB: const int &b{} ‹- не пытается создать неинициализированную ссылку, а привязывает ее к временному целочисленному объекту. Второй пример: struct A { const int &b; A():b{} {} }; ‹- не пытается создать неинициализированную ссылку (как это сделал бы ()), а привязывает ее к временному целочисленному объекту, а затем оставляет его висящим. GCC даже с -Wall не предупреждает для второго примера. - person Johannes Schaub - litb; 18.11.2018
comment
@ JohannesSchaub-litb. Блин, совсем забыл об этом, но мне все равно сложно представить, что это проблема. Конструктор вашего второго примера явно сломан (для некоторого определения, очевидно), поэтому это ошибка в дизайне класса, а не в руководстве (clang даже выдает ошибку). И если вы опустите конструктор (сделайте его POD), вы получите ошибку в момент создания экземпляра. Первый пример не должен быть проблемой, поскольку ссылка должна продлить время жизни временного - нет? - person MikeMB; 18.11.2018
comment
Лично я пошел еще дальше: _1 _... - person Aconcagua; 05.02.2019
comment
Привет, нравится ваш ответ, но что вы делаете в случаях, когда используете какую-то стороннюю библиотеку, когда вы, возможно, не можете быть уверены, что библиотека действительно делает внутри с параметрами конструктора? В любом случае, при таком подходе нужно знать внутреннее устройство библиотеки и нельзя просто рассматривать его как черный ящик на уровне API. Я считаю, что в целом C ++ здесь не очень хорош - синтаксис требует дальнейшего улучшения. - person StPiere; 14.09.2020
comment
Я бы добавил здесь: если вы знаете, что делает библиотека (или имеет / не имеет конструктора initialize_list), используйте этот подход. В противном случае используйте () вместо {} - это может спасти от неприятностей в будущем. - person StPiere; 14.09.2020
comment
@StPiere: Не уверен, что понимаю вашу озабоченность. Если у Foo еще нет std::initialize_list конструктора, но Foo foo{a,b} в любом случае имеет смысл поместить a and b в контейнер / структуру данных foo (в противном случае я бы рекомендовал не использовать {} abyway), тогда я не могу представить причину, по которой потенциальный будущий std Конструктор :: initializer_list изменит семантику этого вызова. Если такой конструктор означает что-то, кроме вышеупомянутого, это, по-моему, просто плохой дизайн шрифта. - person MikeMB; 14.09.2020

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

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

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

person Red XIII    schedule 14.08.2013
comment
Это очень важный момент в универсальном программировании. Когда вы пишете шаблоны, не используйте braced-init-lists (стандартное название для { ... }), если вам не нужна семантика initializer_list (ну и, возможно, для создания объекта по умолчанию). - person Xeo; 14.08.2013
comment
Честно говоря, я не понимаю, почему правило std::initializer_list вообще существует - оно просто добавляет путаницы и путаницы в язык. Что плохого в использовании Foo{{a}}, если вам нужен конструктор std::initializer_list? Это кажется намного проще для понимания, чем приоритет std::initializer_list над всеми другими перегрузками. - person user1520427; 02.02.2015
comment
+1 за вышеупомянутый комментарий, потому что я думаю, что это действительно беспорядок !! это не логика; Foo{{a}} следует некоторой логике для меня гораздо больше, чем Foo{a}, который превращается в приоритет списка инициализаторов (пользователь может подумать, хм ...) - person Gabriel; 19.04.2015
comment
По сути, C ++ 11 заменяет один беспорядок другим. Ой, извините, он не заменяет его - он добавляет к нему. Как узнать, не встречались ли вам такие классы? Что, если вы запустите без std::initializer_list<Foo> конструктора, но он будет добавлен в класс Foo в какой-то момент для расширения его интерфейса? Потом облажались пользователи класса Foo. - person doc; 29.05.2015
comment
Foo c {a} просто вызывает ctor копирования, начиная с CWG 1467 (по крайней мере, реализовано в clang) - person Cubbi; 08.02.2016
comment
.. каковы МНОГО причин использовать инициализацию скобок? В этом ответе указывается одна причина (initializer_list<>), которая на самом деле не соответствует критериям того, кто считает ее предпочтительной, а затем упоминается один хороший случай, когда она НЕ предпочтительна. Что мне не хватает, что еще ~ 30 человек (по состоянию на 21.04.2016) сочли полезными? - person dwanderson; 21.04.2016
comment
Что это за МНОГИЕ причины? - person K.Mulier; 18.11.2017
comment
@Doc: Вы можете сказать то же самое о любой функции: что, если позже будет добавлена ​​более подходящая перегрузка? - person MikeMB; 20.03.2018
comment
Если я скопирую и выполню ваш код, я получу copy ctor\ncopy ctor. Разве список инициализаторов не должен выполняться со второго вызова? --std=c++17. Это изменилось? - person Daniel Stephens; 30.09.2019

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

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

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

person Allan Jensen    schedule 03.10.2019
comment
Я пробовал поиграть на goldbolt.org, чтобы обойти явные или частные конструкторы, используя предоставленный пример кода и сделав тот или иной частным или явным, и был вознагражден соответствующей ошибкой [-и] компилятора. Хотите подкрепить это примером кода? - person Mark Storer; 22.10.2019
comment
Это исправление проблемы, предлагаемой для C ++ 20: open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf - person Allan Jensen; 10.11.2019
comment
Если вы отредактируете свой ответ, чтобы показать, о каких версиях C ++ вы говорите, я был бы рад изменить свой голос. - person Mark Storer; 12.11.2019