Подтвердить что ты не робот

Почему я должен предпочесть идиому "явно типизированного инициализатора", явно указывая тип

Недавно я купил новый эффективный современный С++ от Scott Meyers и прочитал его сейчас. Но я сталкиваюсь с одним, что меня полностью задевает.

В пункте 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 об этой теме, которая частично отвечает на мой вопрос.

Guideline: рассмотрите объявление локальных переменных 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).

4b9b3361

Ответ 1

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

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

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

    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>{};
    

Ответ 2

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

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

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

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

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

bool priority = features(w)[5];

"Функции возвращают индексируемую последовательность некоторых общих" булевых "значений, мы читаем пятую в priority".

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

"Функция возвращает индексируемую последовательность значений, явно конвертируемых в bool, мы читаем пятую в priority".

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

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

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