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