web-dev-qa-db-fra.com

Existe-t-il un bon moyen d'implémenter un type conditionnel avec un cas d'échec par défaut?

Pour la mise en œuvre d'un type conditionnel, j'aime beaucoup std::conditional_t car il garde le code court et très lisible:

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

son utilisation fonctionne de manière assez intuitive:

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

Mais comme il s'agit d'un type purement conditionnel, il doit y avoir un type par défaut - void, dans ce cas. Par conséquent, si N est une autre valeur, ledit type donne:

bit_type<500u> f; // == void

Maintenant, cela ne compile pas, mais le type de rendement est toujours valide.

Ce qui signifie que vous pourriez dire bit_type<500u>* f; et aurait un programme valide!

Existe-t-il donc un bon moyen de laisser la compilation échouer lorsque le cas d'échec d'un type conditionnel est atteint?


Une idée serait immédiatement de remplacer le dernier std::conditional_t avec 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>>>>;

Le problème est que les modèles sont toujours entièrement évalués, ce qui signifie que le std::enable_if_t est toujours entièrement évalué - et cela échouera si N != std::size_t{ 64 }. Urgh.


Ma solution de contournement actuelle est plutôt maladroite en introduisant une structure et 3 using déclarations:

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>, "");

Ce qui fonctionne généralement, mais je n'aime pas ça car il ajoute tellement de choses, autant utiliser la spécialisation de modèle. Il réserve également void comme type spécial - il ne fonctionnera donc pas où void est en fait un rendement d'une branche. Existe-t-il une solution courte et lisible?

14
Stack Danny

Vous pouvez résoudre ce problème en ajoutant un niveau d'indirection, afin que le résultat de conditional_t Le plus à l'extérieur ne soit pas un type mais une métafonction qui nécessite que ::type Lui soit appliqué. Utilisez ensuite enable_if Au lieu de enable_if_t Afin de ne pas accéder au ::type À moins que cela ne soit réellement nécessaire:

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;

Dans cette version, le type dans la branche finale est enable_if<condition, uint64_t> Qui est toujours un type valide, et vous obtenez seulement une erreur si cette branche est réellement prise et enable_if<false, uint64_t>::type Est nécessaire. Lorsque l'une des branches précédentes est prise, vous finissez par utiliser identity<uintNN_t>::type Pour l'un des types d'entiers plus petits, et peu importe que enable_if<false, uint64_t> N'ait pas de type imbriqué (car vous n'utilisez pas il).

18
Jonathan Wakely

Juste pour le plaisir ... que diriez-vous d'utiliser std::Tuple Et std::Tuple_element En évitant du tout std::conditional?

Si vous pouvez utiliser C++ 14 (donc les variables de modèle et la spécialisation des variables de modèle), vous pouvez écrire une variable de modèle pour la taille de conversion/l'index dans le tuple

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; 

alors bit_type devient

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 vous ne pouvez utiliser que C++ 11, vous pouvez développer une fonction bt_index()constexpr qui renvoie la valeur correcte (ou incorrecte).

Vous pouvez vérifier que vous êtes satisfait

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>, "!" );

et que l'utilisation de bit_type avec une dimension non prise en charge

bit_type<42u> * pbt42;

provoquer une erreur de compilation.

- EDIT - Comme suggéré par Jonathan Wakely, si vous pouvez utiliser C++ 20, donc std::ispow2() et std::log2p1(), vous pouvez simplifier beaucoup: vous pouvez éviter bt_index et écrire simplement

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