в предыдущей серии мы изобретали CRTP. А в этой серии мы вернемся обратно к тому с чего начинали и изобретем удивительно прекрасную синтаксическую конструкцию, сравнимую по красоте с концептами.
Пусть наша задача написать свой собственный std::bit_cast
. C++20, конечно, вышел, но до продакшена везде и повсюду ему далеко, так что такая задача вполне может быть.
bit_cast
позволяет посмотреть на значение одного типа, как будто бы это значение совершенно другого типа. Как reinterpret_cast
, только без нарушения strict aliasing rule и провоцирования UB.
В простейшем и очень не правильном виде, bit_cast
реализуется так:
template <class To, class From>
To bit_cast(const From& from) {
To to;
std::memcpy(&to, &from, sizeof(To));
return to;
}
В этой версии не учтены три (а на самом деле четыре) ограничения, которые накладываются на типы To
и From
, чтобы использование такого преобразования было безопасным. А сейчас мы можем написать ерунду вида
auto v = bit_cast<std::vector<int>>(5);
И получить неопределенное поведение.
В самой первой серии мы обсуждали, как работает SFINAE и куда нужно и можно засовывать SFINAE-проверки. Однако мы не очень сильно рассматривали вариант с использованием проверок внутри списка аргументов шаблонов (в угловых скобках, если что!). А это очень и очень удобно. И выглядеть это может примерно так:
template <class T
class = requirements_for_T>
void func(T) {}
где requiremts_for_T
-- те самые жуткие проверки, decltype с enable_if и прочие выражения, с помощью которых мы отбрасывали варинаты специализаций через SFINAE.
Начнем наворачивать ограничения на bit_cast
:
Прежде всего размеры типов From
и To
должны быть одинаковыми.
template <class To,
class From,
class = std::enable_if_t<sizeof(To) == sizeof(From)>>
To bit_cast(const From& from) {
To to;
std::memcpy(&to, &from, sizeof(To));
return to;
}
Далее у нас объявляется переменная типа To
, конструируемая по умолчанию. По-хорошему, надо требовать, чтоб конструктор еще и ничего не делал, но нам как-то все равно, так что пусть варит кофе или пишет в лог.
template <class To,
class From,
class = std::enable_if_t<sizeof(To) == sizeof(From)>,
class = std::enable_if_t<std::is_default_constructible_v<To>>>
To bit_cast(const From& from) {
To to;
std::memcpy(&to, &from, sizeof(To));
return to;
}
И остается само копирование. Для допустимости memcpy
оба типа должны копироваться тривиальным образом
template <class To,
class From,
class = std::enable_if_t<sizeof(To) == sizeof(From)>,
class = std::enable_if_t<std::is_default_constructible_v<To>>,
class = std::enable_if_t<std::is_trivially_copyable_v<From>>,
class = std::enable_if_t<std::is_trivially_copyable_v<To>>>
To bit_cast(const From& from) {
To to;
std::memcpy(&to, &from, sizeof(To));
return to;
}
Мы, конечно, можем собрать через логическое И все 4 ограничения под одним enable_if, но тогда компилятор не скажет нам, какое конкретно из условий было нарушено. Однако, если мы поменяем clang на gcc, ошибки станут менее понятными. А если поменям на msvc -- совершенно бесполезными вида
<source>(21): error C2783: 'To bit_cast(const From &)': could not deduce template argument for '<unnamed-symbol>'
Попробуем улучшить ситуацию.
Прежде всего мы можем дать длинным и уродливым enable_if псевдонимы и сделать этакие предикаты-требования
template <class T, class U>
using HaveSameSize = std::enable_if_t<sizeof(T) == sizeof(U)>;
template <class T>
using IsDefaultConstructible = std::enable_if_t<std::is_default_constructible_v<T>>;
template <class T>
using IsTriviallyCopyable = std::enable_if_t<std::is_trivially_copyable_v<T>>;
С ними запись становится компактнее и меньше отвлекает деталями стандартной библиотеки
template <class To,
class From,
class = HaveSameSize<To, From>,
class = IsDefaultConstructible<To>,
class = IsTriviallyCopyable<From>,
class = IsTriviallyCopyable<To>>
To bit_cast(const From& from)
Мы можем подсахарить эту конструкцию, введя макрос
#define REQUIRED class =
Но можно обойтись и без макроса.
У нашей навернутой конструкции есть существенный недостаток: мы ввели 4 аргумента для шаблона и засунули проверки в значения по умолчанию. Если пользователь нашего кода, будучи самому себе злобным буратиной, укажет эти аргументы явно, все наши проверки отвалятся.
auto v = bit_cast<std::vector<int>, int, int, int, int, int>(5); // компилируется и UB
Но и от этой напасти есть решение: шаблоны можно параметризовывать не только типами, но и значениями:
template<int x>
void foo();
И если мы засунем проверку на место определения нетипового параметра (вместо int в примере выше), то никуда пользователь уже не денется и никак проверку не сломает. А запихивать проверку в тип параметра мы умеем из первой части (как так не умеем? а в возвращаемое значение разве не запихивали? тут то же самое!). Запихиваем! Для этого внесем крохотное изменение в определение псевдонимов для предикатов:
template <class T, class U>
using HaveSameSize = std::enable_if_t<sizeof(T) == sizeof(U), bool>;
template <class T>
using IsDefaultConstructible = std::enable_if_t<std::is_default_constructible_v<T>, bool>;
template <class T>
using IsTriviallyCopyable = std::enable_if_t<std::is_trivially_copyable_v<T>, bool>;
У шаблона enable_if есть второй параметр, определяющий возвращаемый тип.
И код становится веселее.
template <class To,
class From,
HaveSameSize<To, From> = true,
IsDefaultConstructible<To> = true,
IsTriviallyCopyable<From> = true,
IsTriviallyCopyable<To> = true>
To bit_cast(const From& from)
И gcc ошибки выдает интереснее. И даже очень умная IDE по ним прямо в коде может подсветить, какие условия нарушились! CLion и VSCode тоже могут, если что.
Однако, запись требование = true
еще более странная, чем class = требование
. Сразу вопрос: а что если написать false?
Сделаем красиво: вместо bool введем новый тип, содержащий ровно одно значение -- константу с именем required
enum Required {
required = 0
};
template <class T, class U>
using HaveSameSize = std::enable_if_t<sizeof(T) == sizeof(U), Required>;
template <class T>
using IsDefaultConstructible = std::enable_if_t<std::is_default_constructible_v<T>, Required>;
template <class T>
using IsTriviallyCopyable = std::enable_if_t<std::is_trivially_copyable_v<T>, Required>;
И теперь мы пишем просто и ясно:
template <class To,
class From,
HaveSameSize<To, From> = required,
IsDefaultConstructible<To> = required,
IsTriviallyCopyable<From> = required,
IsTriviallyCopyable<To> = required>
To bit_cast(const From& from)
msvc, правда, все также выводит абсолютно бесполезные сообщения. Но сейчас мы поможем и ему.
У нас в шаблоне под проверки используются анонимные параметры. Дадим им имена, и ошибки от msvc сразу же станут полезными:
template <class To,
class From,
HaveSameSize<To, From> same_size = required,
IsDefaultConstructible<To> default_constructibe_to = required,
IsTriviallyCopyable<From> trivially_copy_from = required,
IsTriviallyCopyable<To> trivially_copy_to = required>
To bit_cast(const From& from)
И ура -- в сообщении написано, какая именно проверка провалилась. Вы восхитительны!
<source>(35): error C2672: 'bit_cast': no matching overloaded function found
<source>(35): error C2783: 'To bit_cast(const From &)': could not deduce template argument for 'same_size'
<source>(26): note: see declaration of 'bit_cast'
<source>(40): error C2672: 'bit_cast': no matching overloaded function found
<source>(40): error C2783: 'To bit_cast(const From &)': could not deduce template argument for 'trivially_copy_to'
<source>(26): note: see declaration of 'bit_cast'