c++ - ¿Hay una buena manera de implementar un tipo condicional con un caso de error predeterminado?
c++11 templates (2)
Puede resolver esto agregando un nivel de direccionamiento indirecto, de modo que el resultado del
conditional_t
más externo no sea un tipo, sino una metafunción que necesita que se le aplique
::type
.
Luego use
enable_if
lugar de
enable_if_t
para no acceder al
::type
menos que sea realmente necesario:
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;
En esta versión, el tipo en la rama final es
enable_if<
condition
, uint64_t>
que siempre es un tipo válido, y solo se obtiene un error si esa rama se toma realmente y se
enable_if<false, uint64_t>::type
.
Cuando se toma una de las ramas anteriores, terminas usando la
identity<uintNN_t>::type
para uno de los tipos de enteros más pequeños, y no importa que
enable_if<false, uint64_t>
no tenga un tipo anidado (porque no usalo).
Para implementar un tipo condicional, disfruto mucho de
std::conditional_t
ya que mantiene el código corto y muy legible:
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>>>>;
Su uso funciona de manera bastante intuitiva:
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
Pero dado que este es un tipo condicional puro, debe haber un tipo predeterminado:
void
, en este caso.
Por lo tanto, si
N
es cualquier otro valor, dicho tipo produce:
bit_type<500u> f; // == void
Ahora esto no se compila, pero el tipo de rendimiento sigue siendo válido.
Lo que significa que puedes decir
bit_type<500u>* f;
y tendría un programa válido!
Entonces, ¿hay una buena manera de dejar que la compilación falle cuando se alcanza el caso de falla de un tipo condicional?
Una idea inmediata sería reemplazar el último
std::conditional_t
con
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>>>>;
El problema con eso es que las plantillas siempre se evalúan completamente, lo que significa que
std::enable_if_t
siempre se evalúa completamente, y eso fallará si
N != std::size_t{ 64 }
.
Urgh.
Mi solución alternativa actual a esto es bastante torpe al introducir una estructura y 3
using
declaraciones:
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>, "");
Lo que generalmente funciona, pero no me gusta, ya que agrega muchas cosas, podría usar la especialización de plantillas.
También se reserva el
void
como un tipo especial, por lo que no funcionará donde el
void
sea realmente el rendimiento de una sucursal.
¿Hay una solución legible, corta?
Sólo por diversión ... ¿qué pasa con el uso de
std::tuple
y
std::tuple_element
evitando en absoluto
std::conditional
?
Si puede usar C ++ 14 (para las variables de plantilla y la especialización de las variables de plantilla) puede escribir una variable de plantilla para el tamaño de conversión / índice en la tupla
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;
así que
bit_type
convierte
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>>;
Si solo puede utilizar C ++ 11, puede desarrollar una función
bt_index()
constexpr
que devuelva el valor correcto (o incorrecto).
Puedes verificar que estén satisfechos
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>, "!" );
y que usando
bit_type
con una dimensión no soportada
bit_type<42u> * pbt42;
provocar un error de compilación.
- EDITAR -
Como lo sugiere Jonathan Wakely, si puede usar C ++ 20, entonces
std::ispow2()
y
std::log2p1()
, puede simplificar mucho: puede evitar el
bt_index
y simplemente escribir
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>>;