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

2

15

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?

Gastric answered 8/7, 2019 at 9:26 Comment(4)
Why N == std::size_t{8} instead of N == 8? Are you afraid that std::size_t isn't capable of holding 8?Hashum
@L.F. probably he what to prevent warning of comparing types with different sign.Incrustation
@MarekR Hmm ... I get no warnings with N == 8 under -Wall -Wextra -Werror -pedantic-errors. Maybe the warning is suppressed automatically for literals that presumably do not cause problems?Hashum
I always try keep the number of casts low, I know it's not necessary in a constant compile time context - but it's my habit.Gastric
L
18

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<std::size_t N>
using bit_type = typename
    std::conditional_t<N == std::size_t{  8 }, std::type_identity<std::uint8_t>,
    std::conditional_t<N == std::size_t{ 16 }, std::type_identity<std::uint16_t>,
    std::conditional_t<N == std::size_t{ 32 }, std::type_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 std::type_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).


If you are not using C++20 and std::type_identity is not available, you can make your own:

template<typename T> struct type_identity { using type = T; };
Lappet answered 8/7, 2019 at 9:45 Comment(9)
So this would be a rare case where the _t version actually has a downside. Interesting, thanks!Gastric
::type should be before >>> or use std::std::enable_if_t.Incrustation
@MarekR No, that way conditional operates on the (nonexistent) ::type. And then the entire identity is superfluous.Hashum
@L.F. there is std::conditional_t not std::conditional so ... .Incrustation
@MarekR conditional_t gets the type to apply ::type on, so you would otherwise write typename conditional<...>::type::type. If this sounds confusing, it is!Hashum
@MarekR instead of trying to suggest fixes please compile my code, and try compiling your "fixed" version. You'll find out which one meets the OP's requirement and which one doesn't. My answer does explain why it's done this way, and why it works.Lappet
Ok it compiles but its weird (mixing intX_t with std::enable_if types and adding this indentity instead having only intX_t in the chain). IMO this is simpler: wandbox.org/permlink/7jdTzfjIR8VyN8ZgIncrustation
@MarekR but I don't mix intN_t and enable_if, I mix identity<intN_t> and enable_if<C, intN_t>. Because you need to add a level of indirection. Read the question. Read the answer. Your version is simpler but doesn't work: wandbox.org/permlink/0LKosKXj50Y9hX1jLappet
@MarekR std::conditional works only when its arguments are both valid. In the answer's case, enable_if<...> the type itself is valid (whether it has a type member doesn't affect its validity). In your case, the enable_if_t<...> is invalid because it is equivalent to enable_if<...>::type.Hashum
C
6

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::has_single_bit() (formerly std::ispow2()) and std::bit_width() (formerly 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::has_single_bit(N) ? std::bit_width(N)-4u : -1,
   std::tuple<std::uint8_t, std::uint16_t, std::uint32_t, std::uint64_t>>;
Cyclopentane answered 8/7, 2019 at 10:59 Comment(4)
Neat alternative! I suppose it might increase compilation time for large cases due to std::tuple's allignement nature, which isn't needed here.Gastric
N.B. In C++20 you should be able to do template <std::size_t N> constexpr std::size_t bt_index = std::ispow2(N) ? std::log2p1(N) - 4 : -1; (i.e. without specializing the variable template).Lappet
@JonathanWakely - Exactly what I was looking for; thanks! Given std::ispow2() and std::log2p1() you can avoid bt_index at all and use your expression directly as first argument for std::tuple_element.Cyclopentane
You can indeed :-)Lappet

© 2022 - 2024 — McMap. All rights reserved.