Когда использовать инициализатор, заключенный в скобки?

В С++ 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 FAQ
  std::default_random_engine b;
}
{ // Example 8
  duration<long> a = 5; // Stroustrup FAQ too
  duration<long> b(5);
  duration<long> c {5};
}

Для каждой переменной, которую я объявляю, я должен думать, какой синтаксис инициализации я должен использовать, и это замедляет мою скорость кодирования. Я уверен, что это не было целью введения фигурных скобок.

Когда дело доходит до кода шаблона, изменение синтаксиса может привести к различным значениям, поэтому правильный путь имеет важное значение.

Интересно, существует ли универсальный ориентир, который должен выбрать синтаксис.

Ответ 1

Я думаю, что следующее может быть хорошим ориентиром:

  • Если значение (одиночное), которое вы инициализируете, предназначено для точного значения объекта, используйте copy (=) инициализацию (потому что тогда в случае ошибки вы никогда не будете случайно ссылаться на явный конструктор, который обычно интерпретирует предоставленное значение по-разному). В тех местах, где инициализация копирования недоступна, посмотрите, имеет ли правильность семантики правильность семантики, и если да, используйте ее; в противном случае используйте инициализацию скобок (если она также недоступна, вам все равно не повезло).

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

  • Если значения, которые вы инициализируете, не являются значениями, которые необходимо сохранить, но описывают предполагаемое значение/состояние объекта, используйте круглые скобки. Примерами являются аргумент размера vector или аргумент имени файла fstream.

Ответ 2

Я уверен, что никогда не будет универсального руководства. Мой подход состоит в том, чтобы использовать всегда фигурные скобки, помнящие, что

  • Конструкторы списка инициализаторов имеют приоритет над другими конструкторами
  • Все стандартные библиотечные контейнеры и std:: basic_string имеют конструкторы списка инициализаторов.
  • Изначальная фигурная скобка не допускает сужения конверсий.

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

Ответ 3

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

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 относится к прямой инициализации, тогда как мы используем инициализацию скобок, которая откладывает для прямой инициализации тогда и только тогда, когда нет конструктора, принимающего std::initializer_list. Аналогично std::is_convertible не помогает.

Я исследовал, действительно ли можно отбросить черту, которая может исправить это, но я не слишком оптимистично отношусь к этому. Во всяком случае, я не думаю, что мы много не хватаемся, я думаю, что тот факт, что make_unique<T>(foo, bar) приводит к конструкции, эквивалентной T(foo, bar), очень интуитивно понятен; особенно учитывая, что make_unique<T>({ foo, bar }) довольно несходна и имеет смысл только в том случае, если foo и bar имеют один и тот же тип.

Следовательно, для общего кода я использую только фигурные скобки для инициализации значения (например, T t {}; или T t = {};), что очень удобно, и я думаю, что лучше, чем путь С++ 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.