Коли використовувати ініціалізатор, укладений в дужки?


94

У C ++ 11 у нас є новий синтаксис для ініціалізації класів, який дає нам велику кількість можливостей ініціалізації змінних.

{ // Example 1
  int b(1);
  int a{1};
  int c = 1;
  int d = {1};
}
{ // Example 2
  std::complex<double> b(3,4);
  std::complex<double> a{3,4};
  std::complex<double> c = {3,4};
  auto d = std::complex<double>(3,4);
  auto e = std::complex<double>{3,4};
}
{ // Example 3
  std::string a(3,'x');
  std::string b{3,'x'}; // oops
}
{ // Example 4
  std::function<int(int,int)> a(std::plus<int>());
  std::function<int(int,int)> b{std::plus<int>()};
}
{ // Example 5
  std::unique_ptr<int> a(new int(5));
  std::unique_ptr<int> b{new int(5)};
}
{ // Example 6
  std::locale::global(std::locale("")); // copied from 22.4.8.3
  std::locale::global(std::locale{""});
}
{ // Example 7
  std::default_random_engine a {}; // Stroustrup's FAQ
  std::default_random_engine b;
}
{ // Example 8
  duration<long> a = 5; // Stroustrup's FAQ too
  duration<long> b(5);
  duration<long> c {5};
}

Для кожної змінної, яку я оголошу, я повинен думати, який ініціалізаційний синтаксис я повинен використовувати, і це уповільнює швидкість кодування. Я впевнений, що не було наміру вводити фігурні дужки.

Що стосується коду шаблону, зміна синтаксису може призвести до різних значень, тому важливим є правильний шлях.

Цікаво, чи існує універсальна настанова, який синтаксис слід обрати.


1
Приклад ненавмисної поведінки від {} ініціалізації: string (50, 'x') vs string {50, 'x'} тут
P i

Відповіді:


64

Я думаю, що наступне може бути хорошим орієнтиром:

  • Якщо значення (одиночне), з яким ви ініціалізуєте, має бути точним значенням об'єкта, використовуйте =ініціалізацію copy ( ) (тому що тоді у випадку помилки ви ніколи не випадково викликаєте явний конструктор, який, як правило, інтерпретує надане значення по-різному). У місцях, де ініціалізація копіювання недоступна, дивіться, чи є ідентифікація дужок правильною семантикою, і якщо так, використовуйте це; в іншому випадку використовуйте ініціалізацію дужок (якщо це також недоступно, у будь-якому випадку вам не пощастить).

  • Якщо значення, з якими ви ініціалізуєтесь, - це список значень, які слід зберігати в об'єкті (наприклад, елементи вектора / масиву або реальна / уявна частина складного числа), використовуйте фігурну дужку ініціалізації, якщо вона є.

  • Якщо значення, з якими ви ініціалізуєтесь, не є значеннями, які слід зберігати, але описують призначене значення / стан об'єкта, використовуйте дужки. Прикладами є аргумент розміру vectorабо аргумент імені файлу fstream.


4
@ user1304032: Локал не є рядком, тому ви не використовуєте ініціалізацію копіювання. Локал також не містить рядок (він може зберігати цей рядок як деталь реалізації, але це не його мета), тому ви не використовуєте ініціалізацію дужок. Тому керівництво говорить про використання ініціалізації дужок.
celtschk

2
Мені особисто це найбільше сподобалось, і він також добре працює на загальному коді. Є деякі винятки ( T {}або синтаксичні причини, як, наприклад, самий неприємний синтаксичний аналіз ), але в цілому, я думаю, що це є гарною порадою. Зауважте, що це моя суб'єктивна думка, тому і на інші відповіді слід дивитись.
хеламі

2
@celtschk: це не працюватиме для копіюваних, нерухливих типів; type var{};робить.
ildjarn

2
@celtschk: Я не кажу, що це трапляється часто, але це менше набирати текст і працює в більшій кількості контекстів, тож у чому мінус?
ildjarn

2
Мої вказівки, безумовно, ніколи не вимагають ініціалізації копіювання. ; -]
ілджарн

26

Я впевнений, що ніколи не буде універсальної настанови. Мій підхід полягає у використанні завжди фігурних брекетів, пам’ятаючи про це

  1. Конструктори списку ініціалізаторів мають перевагу перед іншими конструкторами
  2. Усі стандартні контейнери бібліотеки та std :: basic_string мають конструктори списку ініціалізаторів.
  3. Кучерява ініціалізація дужок не дозволяє звужувати конверсії.

Тож круглі та фігурні брекети не є взаємозамінними. Але знання того, де вони відрізняються, дозволяє в більшості випадків використовувати фігурну ініціалізацію круглих дужок (деякі випадки, коли я зараз не можу бути помилками компілятора).


6
Фігурні дужки мають той недолік, що я можу помилково назвати конструктор списку. Круглі дужки ні. Це не є причиною використання круглих дужок за замовчуванням?
хеламі

4
@user: Щоправда, int i = 0;я не думаю, що хтось би int i{0}там використовував , і це може бути заплутано (також, 0якщо це тип int, тож звуження не було б ). У всьому іншому я б дотримувався порад Хуанчо: віддайте перевагу {}, остерігайтеся кількох випадків, коли цього не слід. Зауважте, що існує не так багато типів, які будуть приймати списки ініціалізаторів як аргументи конструктора, ви можете очікувати, що контейнери та типи контейнерів (кортеж ...) матимуть їх, але більшість кодів викликає відповідний конструктор.
Девід Родрігес - дрибес

3
@ user1304032 це залежить, якщо ви дбаєте про звуження. Я це роблю, тому я віддаю перевагу компілятору сказати мені, що int i{some floating point}це помилка, а не мовчки усікати.
juanchopanza

3
Щодо "віддайте перевагу {}, остерігайтеся кількох випадків, коли цього не потрібно": Скажіть, два класи мають семантично еквівалентний конструктор, але один клас також має список ініціалізатора. Чи слід називати два рівнозначні конструктори по-різному?
хеламі

3
@helami: "Скажімо, два класи мають семантично еквівалентний конструктор, але один клас також має список ініціалізатора. Чи слід називати два еквівалентні конструктори по-різному?" Скажіть, я наткнувся на найбільш неприємний розбір; що може статися на будь-якому конструкторі для будь-якого випадку. Набагато простіше уникнути цього, якщо ви просто використовуєте, {}щоб означати "ініціалізувати", якщо ви абсолютно не можете .
Нікол Болас

16

Поза загальним кодом (тобто шаблонами) ви можете (і я це роблю) використовувати брекети скрізь . Одна перевага полягає в тому, що він працює скрізь, наприклад, навіть для ініціалізації в класі:

struct foo {
    // Ok
    std::string a = { "foo" };

    // Also ok
    std::string b { "bar" };

    // Not possible
    std::string c("qux");

    // For completeness this is possible
    std::string d = "baz";
};

або для аргументів функції:

void foo(std::pair<int, double*>);
foo({ 42, nullptr });
// Not possible with parentheses without spelling out the type:
foo(std::pair<int, double*>(42, nullptr));

Для змінних я не приділяю великої уваги між T t = { init };абоT t { init }; стилями , я вважаю, що різниця є незначною, і в гіршому випадку спричинить лише корисне повідомлення компілятора про неправильне використання explicitконструктора.

Для типів, які приймають, std::initializer_listхоча, очевидно, іноді std::initializer_listпотрібні неконструктори (класичний приклад єstd::vector<int> twenty_answers(20, 42); ). Добре тоді дужки не використовувати.


Що стосується загального коду (тобто в шаблонах), то саме останній абзац повинен був викликати деякі попередження. Розглянемо наступне:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T { std::forward<Args>(args)... } }; }

Потім auto p = make_unique<std::vector<T>>(20, T {});створює вектор розміром 2, якщо Tє, наприклад int, або вектор розміром 20, якщо він Tє std::string. Дуже показовий знак того, що тут відбувається щось дуже неправильне, - це те, що немає жодної риси, яка може вас врятувати тут (наприклад, з SFINAE): std::is_constructibleце з точки зору прямої ініціалізації, тоді як ми використовуємо brace-ініціалізацію, яка відкладає на пряму- ініціалізація, якщо і лише тоді, коли конструктор не std::initializer_listзаважає. Так само std::is_convertibleне допомагає.

Я досліджував, чи насправді можливо прокрутити ознаку, яка може це виправити, але я не надто оптимістичний щодо цього. У будь-якому випадку, я не думаю, що ми б пропустили багато, я вважаю, що факт, що make_unique<T>(foo, bar)призводить до конструкції, еквівалентної T(foo, bar)дуже інтуїтивно зрозумілий; особливо, враховуючи, що make_unique<T>({ foo, bar })це дуже різниться і має сенс лише якщоfoobar мають і той самий тип.

Отже, для загального коду я використовую лише дужки для ініціалізації значень (наприклад, T t {};або T t = {};), що дуже зручно, і я думаю, що перевершує спосіб C ++ 03 T t = T();. Інакше це або синтаксис прямої ініціалізації (тобтоT t(a0, a1, a2); ), або іноді побудова за замовчуванням ( T t; stream >> t;є єдиним випадком, коли я думаю, що я використовую).

Це не означає, що всі дужки погані, врахуйте попередній приклад з виправленнями:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T(std::forward<Args>(args)...) }; }

Це все ще використовує дужки для побудови std::unique_ptr<T>, хоча фактичний тип залежить від параметра шаблону T.


@interjay Для деяких моїх прикладів, можливо, потрібно буде замість цього використовувати непідписані типи, наприклад, make_unique<T>(20u, T {})для Tтого, щоб бути unsignedабо std::string. Не надто впевнений у деталях. (Зауважте, що я також коментував очікування щодо прямої ініціалізації проти ініціалізації брекету щодо напр., Функцій ідеального переадресації.) std::string c("qux");Не було визначено, щоб вона працювала як ініціалізація в класі, щоб уникнути неясностей з деклараціями функцій членів у граматиці.
Люк Дантон

@interjay Я не згоден з вами в першому пункті, не соромтеся перевірити 8.5.4 Ініціалізація списку та 13.3.1.7 Ініціалізація шляхом ініціалізації списку. Щодо другого, тобі потрібно уважніше подивитися на те, що я написав (що стосується ініціалізації в класі ) та / або граматику C ++ (наприклад, член-декларатор , який посилається на ініціалізатор дужки або рівності ).
Люк Дантон

Хм, ти маєш рацію - я тестував GCC 4.5 раніше, що, здавалося, підтверджувало те, що я говорив, але GCC 4.6 з вами згоден. І я пропустив той факт, що ви говорили про ініціалізацію в класі. Мої вибачення.
interjay
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.