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