Ответ 1
Следуя стандарту С++:
§ 8.5 Инициализаторы
[dcl.init]
Инициализация, которая встречается в форме
T x = a;
а также при передаче аргументов, возврату функции, исключении (15.1), обработке исключения (15.3) и инициализации элемента агрегации (8.5.1) называется копирование-инициализация.
Я могу вспомнить пример, приведенный в книге:
auto x = features(w)[5];
как тот, который представляет любую форму инициализации копий с типом auto/template (выводимый тип вообще), так же, как:
template <typename A>
void foo(A x) {}
foo(features(w)[5]);
а также:
auto bar()
{
return features(w)[5];
}
а также:
auto lambda = [] (auto x) {};
lambda(features(w)[5]);
Итак, точка в том, что мы не всегда можем просто "переместить тип T из static_cast<T>
в левую часть назначения".
Вместо этого в любом из приведенных выше примеров нам нужно явно указать желаемый тип, а не позволять компилятору вывести его самостоятельно, если последнее может привести к поведению undefined:
Соответственно моим примерам, которые будут:
/*1*/ foo(static_cast<bool>(features(w)[5]));
/*2*/ return static_cast<bool>(features(w)[5]);
/*3*/ lambda(static_cast<bool>(features(w)[5]));
Таким образом, использование static_cast<T>
- это элегантный способ принудительного ввода желаемого типа, который альтернативно может быть выражен явным вызовом contructor:
foo(bool{features(w)[5]});
Подводя итог, я не думаю, что в книге говорится:
Всякий раз, когда вы хотите принудительно ввести тип переменной, используйте
auto x = static_cast<T>(y);
вместоT x{y};
.
Для меня это звучит скорее как предупреждение:
Вывод типа с
auto
является классным, но может привести к поведению undefined при неправильном использовании.
И как решение для сценариев, связанных с дедукцией типа, предлагается следующее:
Если механизм регулярного вывода типа компилятора не является тем, что вы хотите, используйте
static_cast<T>(y)
.
UPDATE
И отвечая на ваш обновленный вопрос, какой из следующих инициализаций следует предпочесть:
bool priority = features(w)[5];
auto priority = static_cast<bool>(features(w)[5]);
auto priority = bool(features(w)[5]);
auto priority = bool{features(w)[5]};
Сценарий 1
Сначала представьте, что std::vector<bool>::reference
не неявно конвертируется в bool
:
struct BoolReference
{
explicit operator bool() { /*...*/ }
};
Теперь bool priority = features(w)[5];
будет не компилировать, так как это не явный логический контекст. Остальные будут работать нормально (пока доступен operator bool()
).
Сценарий 2
Во-вторых, предположим, что std::vector<bool>::reference
реализован по-старому, и хотя оператор преобразования не explicit
, вместо него он возвращает int
:
struct BoolReference
{
operator int() { /*...*/ }
};
Изменение сигнатуры отключает инициализацию auto priority = bool{features(w)[5]};
, так как использование {}
предотвращает сужение (которое преобразует int
в bool
).
Сценарий 3
В-третьих, что, если мы говорим не о bool
вообще, а о некотором пользовательском типе, что, к нашему удивлению, объявляет конструктор explicit
:
struct MyBool
{
explicit MyBool(bool b) {}
};
Удивительно, но еще раз инициализация MyBool priority = features(w)[5];
будет не компилироваться, так как синтаксис копирования-инициализации требует неявного конструктора. Другие будут работать, хотя.
Личное отношение
Если бы я выбрал одну инициализацию из перечисленных четырех кандидатов, я бы пошел с:
auto priority = bool{features(w)[5]};
потому что он вводит явный логический контекст (это хорошо, если мы хотим присвоить это значение логической переменной) и предотвращает сужение (в случае других типов, не-легко-конвертируемых-на-bool), так что когда выдается сообщение об ошибке/предупреждении, мы можем диагностировать, что features(w)[5]
действительно.
ОБНОВЛЕНИЕ 2
Недавно я смотрел речь Херба Саттера с CppCon 2014 под названием Вернуться к основам! Essentials of Modern С++ Style, где он приводит некоторые соображения о том, почему следует отдавать предпочтение явному инициализатору типа формы auto x = T{y};
(хотя это не то же самое, что с auto x = static_cast<T>(y)
, поэтому не все аргументы применяются) над T x{y};
, которые:
-
auto
переменные всегда должны быть инициализированы. То есть вы не можете написатьauto a;
, как вы можете писать с ошибкойint a;
-
Современный стиль С++ предпочитает тип с правой стороны, как и в:
a) Литералы:
auto f = 3.14f; // ^ float
b) Пользовательские литералы:
auto s = "foo"s; // ^ std::string
c) Объявление функций:
auto func(double) -> int;
d) Именованные лямбда:
auto func = [=] (double) {};
e) Псевдонимы:
using dict = set<string>;
f) Алиасы шаблонов:
template <class T> using myvec = vector<T, myalloc>;
, так что, добавив еще одно:
auto x = T{y};
соответствует стилю, в котором у нас есть имя с левой стороны, и введите с инициализатором с правой стороны, что можно кратко описать как:
<category> name = <type> <initializer>;
-
С помощью copy-elision и неявных конструкторов copy/move он имеет нулевую стоимость по сравнению с синтаксисом
T x{y}
. -
Это более явный, когда существуют тонкие различия между типами:
unique_ptr<Base> p = make_unique<Derived>(); // subtle difference auto p = unique_ptr<Base>{make_unique<Derived>()}; // explicit and clear
-
{}
не гарантирует неявных преобразований и не сужается.
Но он также упоминает некоторые недостатки формы auto x = T{}
в целом, которые уже были описаны в этом сообщении:
-
Несмотря на то, что компилятор может временно удалить правую часть, для этого требуется доступный, не удаленный и неявный конструктор-копир:
auto x = std::atomic<int>{}; // fails to compile, copy constructor deleted
-
Если элиция не включена (например,
-fno-elide-constructors
), то перемещение недвижущихся типов приводит к дорогостоящей копии:auto a = std::array<int,50>{};