Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a nice way to implement a conditional type with default fail case?

For implementing a conditional type I highly enjoy std::conditional_t as it keeps the code short and very readable:

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

using it works quite intuitively:

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

But since this is a pure conditional type there must be a default type - void, in this case. Therefore if N is any other value said type yields:

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

Now this doesn't compile, but the yielding type is still valid.

Meaning you could say bit_type<500u>* f; and would have a valid program!

So is there a nice way to let compilation fail when the fail case of an conditional type is reached?


One idea immediately would be to replace the last std::conditional_t with 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>>>>;

The problem with that is that templates are always fully evaluated, meaning that the std::enable_if_t is always fully evaluated - and that will fail if N != std::size_t{ 64 }. Urgh.


My current go-to workaround to this is rather clumsy introducing a struct and 3 using declarations:

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

Which generally works, but I dislike it as it adds so much stuff, I might as well just use template specialization. It also reserves void as a special type - so it won't work where void is actually a yield from a branch. Is there a readable, short solution?

like image 797
Stack Danny Avatar asked Jul 08 '19 09:07

Stack Danny


2 Answers

You can solve this by adding a level of indirection, so that the result of the outermost conditional_t is not a type but a metafunction that needs ::type to be applied to it. Then use enable_if instead of enable_if_t so you don't access the ::type unless it's actually needed:

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;

In this version the type in the final branch is enable_if<condition, uint64_t> which is always a valid type, and you only get an error if that branch is actually taken and enable_if<false, uint64_t>::type is needed. When one of the earlier branches is taken you end up using identity<uintNN_t>::type for one of the smaller integer types, and it doesn't matter that enable_if<false, uint64_t> has no nested type (because you don't use it).

like image 121
Jonathan Wakely Avatar answered Oct 19 '22 10:10

Jonathan Wakely


Just for fun... what about using std::tuple and std::tuple_element avoiding at all std::conditional?

If you can use C++14 (so template variables and specialization of template variables) you can write a template variable for conversion size/index-in-the-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; 

so bit_type become

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

If you can use only C++11, you can develop a bt_index() constexpr function that return the correct (or incorrect) value.

You can verify that are satisfied

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

and that using bit_type with an unsupported dimension

bit_type<42u> * pbt42;

cause a compilation error.

-- EDIT -- As suggested by Jonathan Wakely, if you can use C++20, so std::ispow2() and std::log2p1(), you can simplify a lot: you can avoid bt_index at all and simply write

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>>;
like image 6
max66 Avatar answered Oct 19 '22 11:10

max66