Почему я должен предпочесть & quot; явно типизированный инициализатор & quot; идиома более явно давая тип

Я недавно купил новый эффективный современный 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).

42

Решение

Следуя стандарту C ++:

§ 8.5 Инициализаторы [dcl.init]

  1. Инициализация, которая происходит в форме

    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};, которые:

  1. auto переменные всегда должны быть инициализированы. То есть ты не можешь писать auto a;так же, как вы можете написать подверженный ошибкам int a;

  2. современный 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>;
    
  3. С копирующими и неявными конструкторами копирования / перемещения он имеет с нулевой стоимостью по сравнению с T x{y} синтаксис.

  4. Это более очевидно, когда между типами есть тонкие различия:

     unique_ptr<Base> p = make_unique<Derived>(); // subtle difference
    
    auto p = unique_ptr<Base>{make_unique<Derived>()}; // explicit and clear
    
  5. {} не гарантирует никаких неявных преобразований и без сужения.

Но он также упоминает некоторые недостатки auto x = T{} Форма в целом, которая уже была описана в этом посте:

  1. Несмотря на то, что компилятор может исключить временную правую часть, он требует доступного, не удаленного и неявного конструктора копирования:

     auto x = std::atomic<int>{}; // fails to compile, copy constructor deleted
    
  2. Если elision не включен (например, -fno-elide-constructors), затем перемещение неподвижных типов приводит к дорогостоящему копированию:

     auto a = std::array<int,50>{};
    
23

Другие решения

Передо мной нет книги, поэтому я не могу сказать, есть ли еще контекст.

Но чтобы ответить на ваш вопрос, нет, используя auto+static_cast в этом конкретном примере не является хорошим решением. Это нарушает другое правило (для которого я никогда не видел оправданных исключений):

  • Используйте самое слабое приведение / преобразование.

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

Здесь static_cast излишне силен. Неявное преобразование будет хорошо. Так что избегайте актерского состава.

15

Контекст из книги:

Хоть std::vector<bool> концептуально bools, operator[] за std::vector<bool> не возвращает ссылку на элемент контейнера (что std::vector::operator[] возвращает для каждого типа, кроме bool). Вместо этого он возвращает объект типа std::vector<bool>::reference (класс, вложенный внутрь std::vector<bool>).

Преимущества нет, это больше предотвращает ошибки, когда вы используете auto с внешней библиотекой.

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

Кстати, здесь хорошая статья на GotW об авто.

8

Может кто-нибудь сказать мне, почему выгодно использовать эту идиому?

Причина, о которой я могу думать: потому что это явно. Подумайте, как бы вы (инстинктивно) прочитали этот код (т.е. не зная, что features делает):

bool priority = features(w)[5];

«Features возвращает индексируемую последовательность некоторых общих» логических «значений; мы читаем пятую в priority».

auto priority = static_cast<bool>(features(w)[5]);

«Feature возвращает индексируемую последовательность значений, явно конвертируемую в bool; мы читаем пятый в priority».

Этот код написан не для оптимизации для самого короткого гибкого кода, а для явного результата (и, видимо, согласованности — поскольку я предполагаю, что это будет не единственная переменная, объявленная с помощью auto).

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

Тем не менее, я бы предпочел версию без явного приведения.

3
По вопросам рекламы [email protected]