Есть хороший способ реализовать условный тип с регистром сбоя по умолчанию?
Для реализации условного типа мне очень нравится std::conditional_t
как он делает код коротким и очень читабельным:
template<std::size_t N>
using bit_type =
std::conditional_t<N == std::size_t{ 8 }, std::uint8_t,
std::conditional_t<N == std::size_t{ 16 }, std::uint16_t,
std::conditional_t<N == std::size_t{ 32 }, std::uint32_t,
std::conditional_t<N == std::size_t{ 64 }, std::uint64_t, void>>>>;
используя это работает довольно интуитивно:
bit_type<8u> a; // == std::uint8_t
bit_type<16u> b; // == std::uint16_t
bit_type<32u> c; // == std::uint32_t
bit_type<64u> d; // == std::uint64_t
Но так как это чисто условный тип, должен быть тип по умолчанию - в данном случае void
. Поэтому, если N
- любое другое значение, указанный тип дает:
bit_type<500u> f; // == void
Теперь это не компилируется, но тип выдачи все еще действителен.
Это означает, что вы можете сказать bit_type<500u>* f;
и будет иметь действующую программу!
Так есть ли хороший способ позволить сбой компиляции при достижении случая сбоя условного типа?
Одна из идей - заменить последний std::conditional_t
на std::enable_if_t
:
template<std::size_t N>
using bit_type =
std::conditional_t<N == std::size_t{ 8 }, std::uint8_t,
std::conditional_t<N == std::size_t{ 16 }, std::uint16_t,
std::conditional_t<N == std::size_t{ 32 }, std::uint32_t,
std::enable_if_t< N == std::size_t{ 64 }, std::uint64_t>>>>;
Проблема заключается в том, что шаблоны всегда полностью оцениваются, а это означает, что std::enable_if_t
всегда полностью оценивается - и это не удастся, если N != std::size_t{ 64 }
. Urgh.
Мой текущий обходной путь к этому довольно неуклюже, представляя структуру и 3 using
объявлений:
template<std::size_t N>
struct bit_type {
private:
using vtype =
std::conditional_t<N == std::size_t{ 8 }, std::uint8_t,
std::conditional_t<N == std::size_t{ 16 }, std::uint16_t,
std::conditional_t<N == std::size_t{ 32 }, std::uint32_t,
std::conditional_t<N == std::size_t{ 64 }, std::uint64_t, void>>>>;
public:
using type = std::enable_if_t<!std::is_same_v<vtype, void>, vtype>;
};
template<std::size_t N>
using bit_type_t = bit_type<N>::type;
static_assert(std::is_same_v<bit_type_t<64u>, std::uint64_t>, "");
Как правило, это работает, но мне не нравится, так как он добавляет так много вещей, что я мог бы просто использовать специализацию шаблонов. Он также резервирует void
как особый тип - поэтому он не будет работать там, где void
- это доход от ветки. Есть ли читаемое, короткое решение?
Ответы
Ответ 1
Вы можете решить эту проблему, добавив уровень косвенности, так что результатом самого внешнего conditional_t
будет не тип, а метафункция, для которой необходимо применить ::type
. Затем используйте enable_if
вместо enable_if_t
чтобы вы не обращались к ::type
если это действительно не нужно:
template<typename T> struct identity { using type = T; };
template<std::size_t N>
using bit_type = typename
std::conditional_t<N == std::size_t{ 8 }, identity<std::uint8_t>,
std::conditional_t<N == std::size_t{ 16 }, identity<std::uint16_t>,
std::conditional_t<N == std::size_t{ 32 }, identity<std::uint32_t>,
std::enable_if<N == std::size_t{ 64 }, std::uint64_t>>>>::type;
В этой версии тип в конечной ветки - это enable_if<
condition
, uint64_t>
которое всегда является допустимым типом, и вы получите ошибку только в том случае, если эта ветвь фактически взята и enable_if<false, uint64_t>::type
требуется. Когда берется одна из более ранних ветвей, вы в конечном итоге используете identity<uintNN_t>::type
для одного из целочисленных типов меньшего размера, и не имеет значения, что enable_if<false, uint64_t>
не имеет вложенного типа (потому что вы не используй это).
Ответ 2
Просто для удовольствия... как насчет использования std::tuple
и std::tuple_element
избегая вообще std::conditional
?
Если вы можете использовать С++ 14 (то есть переменные шаблона и специализация переменных шаблона), вы можете написать переменную шаблона для размера преобразования/индекса в кортеже
template <std::size_t>
constexpr std::size_t bt_index = 100u; // bad value
template <> constexpr std::size_t bt_index<8u> = 0u;
template <> constexpr std::size_t bt_index<16u> = 1u;
template <> constexpr std::size_t bt_index<32u> = 2u;
template <> constexpr std::size_t bt_index<64u> = 3u;
так что bit_type
станет
template <std::size_t N>
using bit_type = std::tuple_element_t<bt_index<N>,
std::tuple<std::uint8_t, std::uint16_t, std::uint32_t, std::uint64_t>>;
Если вы можете использовать только С++ 11, вы можете разработать bt_index()
constexpr
функцию, которая возвращает правильное (или неправильное) значение.
Вы можете проверить, что удовлетворены
static_assert( std::is_same_v<bit_type<8u>, std::uint8_t>, "!" );
static_assert( std::is_same_v<bit_type<16u>, std::uint16_t>, "!" );
static_assert( std::is_same_v<bit_type<32u>, std::uint32_t>, "!" );
static_assert( std::is_same_v<bit_type<64u>, std::uint64_t>, "!" );
и что использование bit_type
с неподдерживаемым измерением
bit_type<42u> * pbt42;
вызвать ошибку компиляции.
- РЕДАКТИРОВАТЬ - Как предлагает Джонатан Уэйкли, если вы можете использовать С++ 20, поэтому std::ispow2()
и std::log2p1()
вы можете сильно упростить: вы можете вообще избежать bt_index
и просто написать
template <std::size_t N>
using bit_type = std::tuple_element_t<std::ispow2(N) ? std::log2p1(N)-4u : -1,
std::tuple<std::uint8_t, std::uint16_t, std::uint32_t, std::uint64_t>>;