Я изучаю современный C++ и сейчас читаю о шаблонах и автоматическом выводе типов.
В качестве задачи я попытался написать функцию из книги для решения проблемы, и в итоге она очень незначительно отличалась от того, что предлагалось в книге.
Упражнение: Напишите функцию-шаблон, которая возвращает целое значение перечисления.
Моя функция:
template<typename T>
constexpr auto EnumToIntegral(T enumerator) noexcept
{
return std::underlying_type_t<T>(enumerator);
}
Функция книги:
template<typename T>
constexpr auto EnumToIntegral(T enumerator) noexcept
{
return static_cast<std::underlying_type_t<T>>(enumerator);
}
Единственная разница здесь — это static_cast
, смысл которого я не могу понять.
Обе функции компилируются и выполняются как положено.
Глядя на другие фрагменты сообщений, посвященных получению целочисленных перечисляемых значений, я также вижу использование static_cast
, но мне не удалось найти для этого никаких оснований.
Я пробовал и с static_cast
, и без него, но результат один и тот же.
Значения перечисления имеют тип int
, а функция, использующая этот результат, — std::get
.
ОТ: стандартное решение — std::to_underlying, если у вас C++23
Первая (ваша версия) — это приведение в стиле C. Старый, который не рекомендуется использовать в C++. По сути, вы выполняете (int)MyEnum, который преобразует константу перечислителя в ее значение int
. Второй, используя static_cast
, выполняет «правильное» приведение константы перечислителя к int
.
Спасибо, это имеет смысл! Должен ли я ответить на свой вопрос, чтобы закрыть это?
@HoraceNess Хотя это и не реальная ситуация с вашим вопросом, поскольку примитивы «просты» (и вам вполне может сойти с рук приведение в стиле C), если вы хотите прочитать о реальных различиях на практике, связанных с приведением типов объектов, вот материал для ночного чтения: Когда следует использовать static_cast, Dynamic_cast, const_cast и reinterpret_cast? и Обычный каст, static_cast и динамический_cast . Это может расширить ваши взгляды на каждую ситуацию и использовать их, когда они вам понадобятся, в зависимости от сценария.
Главное, что он делает, это меняет тип выражения на std::underlying_type<T>
;-). если вы спросите: «Что он делает такого, чего не делает приведение в стиле C», то ответ будет «ничего». Приведение в стиле C здесь является статическим.
Основными приведениями в C++ являются static_cast
, const_cast
, reinterpret_cast
и dynamic_cast
.
Каждый из них может приводить выражения определенных типов и категорий значений к определенным другим типам. Наборы преобразований, разрешенные каждым из них, различны и иногда пересекаются с разной семантикой.
В языке также существует явное преобразование типов (в нотации приведения), более известное как «приведение в стиле C» для формы синтаксиса, полученной из C. Однако, за одним небольшим исключением, это всего лишь комбинация ранее упомянутых типов.
Такое выражение приведения выполнит первую возможную последовательность приведения из следующего списка:
const_cast
static_cast
static_cast
, а затем const_cast
reinterpret_cast
reinterpret_cast
, а затем const_cast
Кроме того, это выражение приведения позволяет static_cast
в этом выборе игнорировать доступность базовых классов, т. е. оно может преобразовать указатель производного класса в указатель частного базового класса, что не позволяет ни одна другая конструкция в языке.
Итак, приведения в стиле C опасны, потому что они могут привести почти все ко всему остальному с трудом, чтобы быть уверенным в семантике и избежать ошибок.
Проблема теперь в том, что существуют две разные формы синтаксиса для явного преобразования типов, которые разделяют семантику приведения в стиле C:
(T)e
(обозначение приведения, также известное как «приведение в стиле C»)T(e)
(функциональное обозначение)где T
— целевой тип и e
исходное выражение. Первая форма широко известна, поскольку именно ее использует синтаксис C. Однако второй вариант, специфичный для C++, по-прежнему имеет ту же семантику явного преобразования типов, что и приведение типов в стиле C, но сбивает с толку, поскольку он похож на
T(e1, e2)
T()
T{e}
где e1
и e2
снова являются выражениями. Хотя формально выражения преобразования типов все еще являются явными, они не выполняют последовательность приведения, как упоминалось выше, а вместо этого получают результат посредством прямой инициализации объявления воображаемой переменной той же формы. Только если одно выражение используется в круглых, а не фигурных скобках, это явное преобразование типа с упомянутыми выше полномочиями.
Итак, если кто-то хочет последовательно избегать приведения в стиле C, ему также следует избегать выражений формы T(e)
.
Но это то, что вы используете. static_cast
более очевиден и указывает, какой фундаментальный тип приведения вам нужен, а не то, что компилятор должен попытаться использовать список, упомянутый выше, и потенциально сделать неправильный выбор.
В этом конкретном примере нет никакой разницы, поскольку выражение с перечисляемым типом всегда можно привести к базовому типу с помощью static_cast
, так что гарантированно будет выбрана вторая запись в списке. Но это может быть неочевидно.
На самом деле форма T{e}
(инициализация прямого списка), как правило, даже слабее, чем static_cast
, но все же сильнее, чем неявное преобразование. Но здесь этого будет недостаточно, поскольку оно не позволяет выполнять преобразование для типов перечислений с ограниченной областью действия.
(За исключением инициализации с помощью фигурных скобок, все это не имеет ничего общего с современными функциями C++, кстати. Эти правила присутствовали со времен первого стандарта C++ C++98.)
Приведения в стиле C — это не столько абстракция, сколько путаница более конкретных приведений.
@Spencer Да, это был неправильный выбор слов.
T(e)
— это не актерский состав в стиле C, если вы это предлагаете. Приведения функциональных стилей не существуют в C. Редактировать: я прочитал это немного более внимательно и не думаю, что вы это предлагаете, но это не на 100% ясно.
@TedLyngmo Нет, это так. Кажется, это малоизвестно (по понятным причинам, как я уже упоминал в своем ответе): eel.is/c++draft/expr.type.conv#2.sentence-1 Например, using T = int&; float x = 0; int& y = T(x);
скомпилирует и вызовет UB при использовании y
, потому что это reinterpret_cast. Обычно это не так уж и плохо, поскольку функциональная нотация не позволяет использовать спецификаторы составного типа непосредственно в синтаксисе.
@TedLyngmo Я не говорю, что он существует на C, кстати. Я использую «приведение в стиле C» для обозначения семантики «явного преобразования типов», как это называется в стандарте. Я думаю, что «приведение в стиле C» можно также интерпретировать как конкретную синтаксическую форму, используемую в C, а не как лежащую в ее основе семантику в C++.
Да, понял. Это эквивалентно приведению в стиле C, поэтому, возможно, его можно было бы назвать эквивалентом приведения в стиле C или чем-то подобным, чтобы не создавать впечатления, что приведение действительно работает в C.
Некоторые люди предпочитают
static_cast
вместо «омни-приведения» большого молотка (приведение в стиле C — даже если вы используете приведение в стиле «функция», это все равно приведение в стиле C).