Я недавно купил новый эффективный современный C ++ от Скотта Мейерса и сейчас читаю его. Но я сталкиваюсь с одной вещью, которая полностью меня беспокоит.
В пункте 5 Скотт говорит, что используя auto
это отличная вещь. Это экономит набор текста, дает вам в большинстве случаев правильный тип и может быть невосприимчивым к несоответствиям типов. Я полностью понимаю это и думаю о auto
как хорошая вещь тоже.
Но затем в пункте 6 Скотт говорит, что у каждой монеты есть две стороны. И также могут быть случаи, когда auto
выводит совершенно неправильный тип, например для прокси-объектов.
Возможно, вы уже знаете этот пример:
class Widget;
std::vector<bool> features(Widget w);
Widget w;
bool priority = features(w)[5]; // this is fine
auto priority = features(w)[5]; // this result in priority being a proxy
// to a temporary object, which will result
// in undefined behavior on usage after that
// line
Все идет нормально.
Но решение Скотта к этому, это так называемая «идиома явно инициализированного инициализатора». Идея состоит в том, чтобы использовать static_cast в инициализаторе следующим образом:
auto priority = static_cast<bool>(features(w)[5]);
Но это не только приводит к большему количеству печатания, но вы также явно указываете тип, который должен быть выведен. Вы в основном потеряли оба преимущества auto
над явным данным типом.
Может кто-нибудь сказать мне, почему выгодно использовать эту идиому?
Сначала, чтобы прояснить ситуацию, мои вопросы направлены на то, почему я должен написать:
auto priority = static_cast<bool>(features(w)[5]);
вместо:
bool priority = features(w)[5];
@Sergey поднял ссылку на хорошую статью о GotW об этой теме, которая частично отвечает на мой вопрос.
Рекомендация: рассмотрите возможность объявления локальных переменных auto x = type {expr}; когда вы хотите явно зафиксировать тип. Самодокументирование показывает, что код явно запрашивает преобразование, гарантирует, что переменная будет инициализирована, и не допускает случайного неявного сужающего преобразования. Только когда вы хотите явного сужения, используйте () вместо {}.
Что в основном подводит меня к связанному вопросу. Какой из этих четыре альтернативы я должен выбрать?
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]};
Номер один по-прежнему мой любимый. Он менее типичный и такой же явный, как и три других.
Пункт о гарантированной инициализации на самом деле не выполняется, так как я в любом случае объявляю переменные не раньше, чем смогу их инициализировать. И другой аргумент о сужении не сработал в быстром тесте (см. http://ideone.com/GXvIIr).
Следуя стандарту C ++:
§ 8.5 Инициализаторы
[dcl.init]
Инициализация, которая происходит в форме
T x = a;
а также при передаче аргументов вызывается функция return, выбрасывание исключения (15.1), обработка исключения (15.3) и инициализация агрегатного члена (8.5.1) копия инициализация.
Я могу вспомнить пример, приведенный в книге:
auto x = features(w)[5];
как тот, который представляет собой любую форму копия инициализация с авто / типом шаблона (выведенный тип в общем) просто так:
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>
в левой части назначения «.
Вместо этого в любом из приведенных выше примеров нам нужно явно указать желаемый тип, а не позволять компилятору выводить его самостоятельно, если последний может привести к неопределенное поведение:
Соответственно моим примерам это будет:
/*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>
это элегантный способ форсирования желаемого типа, который альтернативно может быть выражен явным вызовом конструктора:
foo(bool{features(w)[5]});
Подводя итог, я не думаю, что книга говорит:
Всякий раз, когда вы хотите форсировать тип переменной, используйте
auto x = static_cast<T>(y);
вместоT x{y};
,
Для меня это больше похоже на предупреждение:
Вывод типа с
auto
это круто, но может привести к неопределенному поведению, если использовать его неразумно.
И как решение для сценариев с учетом вычета типа, предлагается следующее:
Если обычный механизм вывода типов в компиляторе не тот, который вам нужен, используйте
static_cast<T>(y)
,
ОБНОВИТЬ
И отвечая на ваш обновленный вопрос, какую из приведенных ниже инициализаций следует предпочесть:
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 титулованный Вернуться к истокам! Основы современного стиля C ++, где он представляет некоторые моменты о том, почему следует отдавать предпочтение явный инициализатор типа из auto x = T{y};
форма (хотя это не то же самое, что с auto x = static_cast<T>(y)
так что не все аргументы применимы) T x{y};
, которые:
auto
переменные всегда должны быть инициализированы. То есть ты не можешь писать auto a;
так же, как вы можете написать подверженный ошибкам int a;
современный C ++ стиль предпочитает тип справа, как в:
а) Литералы:
auto f = 3.14f;
// ^ float
б) Пользовательские литералы:
auto s = "foo"s;
// ^ std::string
в) объявления функций:
auto func(double) -> int;
г) Именованные лямбды:
auto func = [=] (double) {};
д) Псевдонимы:
using dict = set<string>;
е) Псевдонимы шаблона:
template <class T>
using myvec = vector<T, myalloc>;
так как таковой, добавив еще один:
auto x = T{y};
согласуется со стилем, где у нас есть имя с левой стороны, и введите с инициализатором с правой стороны, что можно кратко описать так:
<category> name = <type> <initializer>;
С копирующими и неявными конструкторами копирования / перемещения он имеет с нулевой стоимостью по сравнению с 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
Если elision не включен (например, -fno-elide-constructors
), затем перемещение неподвижных типов приводит к дорогостоящему копированию:
auto a = std::array<int,50>{};
Передо мной нет книги, поэтому я не могу сказать, есть ли еще контекст.
Но чтобы ответить на ваш вопрос, нет, используя auto
+static_cast
в этом конкретном примере не является хорошим решением. Это нарушает другое правило (для которого я никогда не видел оправданных исключений):
Неоправданно сильное приведение приводит к подрыву системы типов и препятствует генерированию диагностическими сообщениями компилятора в случае изменения в другом месте программы, которое несовместимо влияет на преобразование. (действие на расстоянии, буги-вожак по программированию обслуживания)
Здесь static_cast
излишне силен. Неявное преобразование будет хорошо. Так что избегайте актерского состава.
Контекст из книги:
Хоть
std::vector<bool>
концептуальноbool
s,operator[]
заstd::vector<bool>
не возвращает ссылку на элемент контейнера (чтоstd::vector::operator[]
возвращает для каждого типа, кромеbool
). Вместо этого он возвращает объект типаstd::vector<bool>::reference
(класс, вложенный внутрьstd::vector<bool>
).
Преимущества нет, это больше предотвращает ошибки, когда вы используете auto с внешней библиотекой.
Я думаю, это главная идея такой идиомы. Вы должны быть явными и заставить авто вести себя правильно.
Кстати, здесь хорошая статья на GotW об авто.
Может кто-нибудь сказать мне, почему выгодно использовать эту идиому?
Причина, о которой я могу думать: потому что это явно. Подумайте, как бы вы (инстинктивно) прочитали этот код (т.е. не зная, что features
делает):
bool priority = features(w)[5];
«Features возвращает индексируемую последовательность некоторых общих» логических «значений; мы читаем пятую в priority
».
auto priority = static_cast<bool>(features(w)[5]);
«Feature возвращает индексируемую последовательность значений, явно конвертируемую в bool
; мы читаем пятый в priority
».
Этот код написан не для оптимизации для самого короткого гибкого кода, а для явного результата (и, видимо, согласованности — поскольку я предполагаю, что это будет не единственная переменная, объявленная с помощью auto).
Использование авто в декларации priority
для того, чтобы код оставался гибким для любого выражения справа.
Тем не менее, я бы предпочел версию без явного приведения.