有没有一种很好的方法来实现带有默认失败案例的条件类型?

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

对于实现条件类型,我非常喜欢 std::conditional_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::conditional_t<N == std::size_t{ 64 }, std::uint64_t, void>>>>;

使用起来非常直观:

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

但由于这是一个纯条件类型,因此必须有一个默认类型 - void,在本例中。因此,如果 N 是任何其他值,则表示类型产生:

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

现在无法编译,但生成类型仍然有效。

意味着你可以说 bit_type<500u>* f; 并且会有一个有效的程序!

那么有什么好的方法可以让编译在达到条件类型的失败情况时失败吗?


一个想法是将最后一个 std::conditional_t 替换为 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>>>>;

问题是模板总是被完全评估,这意味着 std::enable_if_t 总是被完全评估——如果 N != std::size_t{ 64 } 会失败。呃。


我目前的解决方法是相当笨拙地引入一个结构和 3 个 using 声明:

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

这通常有效,但我不喜欢它,因为它添加了太多东西,我还不如使用模板专业化。它还将 void 保留为特殊类型 - 因此它在 void 实际上是分支的收益的情况下不起作用。有可读的、简短的解决方案吗?

您可以通过添加一个间接级别来解决这个问题,这样最外层 conditional_t 的结果就不是类型而是需要应用 ::type 的元函数。然后使用 enable_if 而不是 enable_if_t 这样您就不会访问 ::type 除非确实需要它:

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;

在此版本中,最终分支中的类型是 enable_if<<i>condition</i>, uint64_t>,它始终是有效类型,并且只有在实际采用该分支并且需要 enable_if<false, uint64_t>::type 时才会出现错误。当采用较早的分支之一时,您最终使用 identity<uintNN_t>::type 作为一种较小的整数类型,并且 enable_if<false, uint64_t> 没有嵌套类型并不重要(因为您不使用它) .

只是为了好玩...使用 std::tuplestd::tuple_element 完全避免 std::conditional 怎么样?

如果你可以使用 C++14(所以模板变量和模板变量的特化)你可以写一个模板变量进行转换 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; 

所以bit_type变成

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

如果你只能使用 C++11,你可以开发一个 bt_index() constexpr 函数,return 正确(或不正确)的值。

您可以验证是否满意

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

以及使用 bit_type 和不支持的维度

bit_type<42u> * pbt42;

导致编译错误。

-- 编辑 -- 正如 Jonathan Wakely 所建议的,如果你可以使用 C++20,那么 std::ispow2()std::log2p1(),你可以简化很多:你可以完全避免 bt_index 并简单地写

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