Почему я должен предпочесть явно типизированную идиому инициализатора явному заданию типа

Недавно я купил новую книгу «Эффективный современный 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]};

Номер один по-прежнему мой любимый. Он менее типичен и столь же ясен, как и три других.

Пункт о гарантированной инициализации на самом деле не имеет места, так как я все равно объявляю переменные не раньше, чем смогу их как-то инициализировать. И другой аргумент о сужении не сработал в быстром тесте.


person Mario    schedule 01.09.2014    source источник
comment
Я читал его книги (не эту), и сомневаюсь, что он ничего не объяснял.   -  person BЈовић    schedule 01.09.2014
comment
@Niall: Насколько я понимаю, вопрос не в том, зачем это нужно, а в том, зачем помещать тип в static_cast, а не вместо auto   -  person Piotr Skotnicki    schedule 01.09.2014
comment
@ПиотрС. именно это моя точка зрения.   -  person Mario    schedule 01.09.2014
comment
@BЈовић, к сожалению, нет. Он добавил побочное примечание для другого случая, когда вы намеренно должны преобразовать значение из числа double в число int.   -  person Mario    schedule 01.09.2014
comment
@ПиотрС. Зачем помещать тип в static_cast объясняется в комментарии (т.е. чтобы избежать UB). Я думаю, что автор пытается сказать использовать свое решение только для конкретных ситуаций в качестве иллюстрированного примера. Нет необходимости использовать его, когда использование auto не вызывает никаких последствий.   -  person 101010    schedule 01.09.2014
comment
@40two: неа... вопрос в том, почему Скотт изобрел идиому, которая выглядит так: auto x = static_cast<Y>(z); для того, что можно выразить как Y x = z;   -  person Piotr Skotnicki    schedule 01.09.2014
comment
И Y x=e; лучше, потому что auto x=static_cast<Y>(e); активирует explicit конверсии, которые следует использовать с осторожностью. Я подозреваю, что аннотация, говорящая, что возвращаемое значение не должно сохраняться после вызванной строки, может быть уместна для C++.   -  person Yakk - Adam Nevraumont    schedule 01.09.2014
comment
О вашем тесте на сужение: этот код не должен компилироваться. Проверяемый вами компилятор неправильно реализует инициализацию фигурных скобок. Предположительно исправят. rextester.com/SFTL92198   -  person bames53    schedule 01.09.2014


Ответы (4)


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

§ 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> в левую часть присваивания".

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

Соответственно моим примерам это будет:

/*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 под названием Назад к основы! Essentials of Modern C++ Style, где он рассказывает о том, почему следует предпочесть явный инициализатор типа в форме 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>;
    

    f) Псевдонимы шаблонов:

    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. Если исключение не включено (например, -fno-elide-constructors), то перемещение неперемещаемых типов приводит к дорогостоящему копированию:

     auto a = std::array<int,50>{};
    
person Piotr Skotnicki    schedule 01.09.2014
comment
Спасибо за ваш вклад, в этих случаях я вижу некоторые преимущества использования static_cast, но я не понимаю, почему это связано с моим первоначальным вопросом? Я имею в виду, что только потому, что где-то есть auto, это не значит, что вы должны подчиняться тем же правилам. - person Mario; 01.09.2014
comment
@Mario: потому что применяются те же правила вывода типа. - person Piotr Skotnicki; 01.09.2014
comment
@Mario: я не думаю, что в книге сказано: всякий раз, когда вы хотите принудительно указать тип переменной, используйте auto x = static_cast<T>(y); вместо T x{y};. На мой взгляд, это всего лишь подсказка, которую книга дает, как форсировать желаемый тип, пропуская обычный механизм вывода типа. - person Piotr Skotnicki; 01.09.2014
comment
Я все еще не уверен, что это хорошая идея для данного кода, но я понимаю вашу точку зрения для других случаев. Это может быть то, что Скотт имел в виду. - person Mario; 01.09.2014
comment
Представляется бессмысленным сравнивать эти четыре auto x = y as T случая и избегать T x{y};... - person Ben Voigt; 01.09.2014
comment
Это не бессмысленно, если мы говорим о синтаксисе инициализации копирования в целом (см. мои первые абзацы), а не о простом присваивании, таком как auto x = z;, а, например. вывод аргумента шаблона, который следует тем же правилам, что и auto. - person Piotr Skotnicki; 01.09.2014
comment
Я думаю, было бы неплохо обновить ваш ответ, включив в него гарантированное удаление копии (open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0135r0.html), который теперь является частью C++17, что позволяет чаще использовать авто . - person Curious; 05.07.2016

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

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

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

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

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

person Ben Voigt    schedule 01.09.2014
comment
Мне очень нравится ваш ответ, но у меня такое чувство, что ответ @Piotr S. больше направлен на то, как это понимается в книге. - person Mario; 01.09.2014
comment
@Mario: явный вызов конструктора T var{expression} действительно лучше, потому что он не нарушает систему типов так сильно, как static_cast. И, где возможно, T var = expression; может быть предпочтительнее, потому что он еще слабее, хотя ненужное разрешение явного вызова конструктора, возможно, намного менее проблематично, чем введение приведения. - person Ben Voigt; 01.09.2014

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

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

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

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

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

person Sergei Nikulov    schedule 01.09.2014
comment
Ну, std::vector<T>::operator[] всегда возвращает std::vector<T>::reference; просто так получилось, что std::vector<bool>::reference не bool& :) (замените на const_reference и bool const& на std::vector<T>::operator[] const). - person Lightness Races in Orbit; 02.09.2014

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

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

bool priority = features(w)[5];

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

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

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

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

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

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

person utnapistim    schedule 01.09.2014
comment
За исключением того, что (для параллелизма со вторым утверждением) первое — это значения, совместимые с bool, не обязательно уже bool. Или это было то, что вы делали, используя логическое значение против bool? - person Ben Voigt; 01.09.2014
comment
Использование auto в объявлении приоритета предназначено для обеспечения гибкости кода для любого выражения в правой части, но может ли auto создать любой тип, отличный от bool, при инициализации выражением static_cast<bool>(...)? Я хочу сказать. Конечно, нет, в таком случае нет никакого преимущества в выборе auto, но у C++ есть некоторые странные углы. - person j_random_hacker; 01.09.2014
comment
@j_random_hacker, правда в том, что я сам не совсем уверен (т.е. я бы, вероятно, объявил переменную как bool и покончил с этим). Одно из преимуществ сохранения static_cast (я только что подумал об этом): при рефакторинге явное приведение может указывать на то, что вызов features должен быть извлечен в функцию, возвращающую bool (особенно если конструкция повторяется в клиентском коде). - person utnapistim; 01.09.2014
comment
@BenVoigt, да, это то, что я имел в виду (я отредактировал ответ, чтобы сделать его немного яснее). Однако мои интерпретации клиентского кода довольно субъективны. - person utnapistim; 01.09.2014