Skip to content

Instantly share code, notes, and snippets.

@Nekrolm
Last active November 4, 2020 08:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nekrolm/5d108980f9658c9206dcdf486141555f to your computer and use it in GitHub Desktop.
Save Nekrolm/5d108980f9658c9206dcdf486141555f to your computer and use it in GitHub Desktop.
cpp14_17_traits_4

C++14/17 type_traits. Часть 4.

в предыдущей серии мы изобретали 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'

Полный пример

Переходите скорее на C++20, чтобы не изобретать подобную красоту!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment