c++ - ¿Hay una buena manera de implementar un tipo condicional con un caso de error predeterminado?




c++11 templates (2)

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?


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).


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>>;




conditional-types