如何让static_assert与SFINAE一起玩得很好

How to make static_assert play nice with SFINAE

本文关键字:一起 SFINAE 很好 assert static      更新时间:2023-10-16

更新

我发布了rebind的工作草稿作为对问题的回答。虽然我没有太多运气找到一种通用方法来防止static_assert破坏元函数。


基本上我想检查是否可以从其他类型T<V, Args...>构造模板化类型T<U, Args...>。其中TArgs...在两种类型中相同。问题是,T<>可能有一个完全破坏我的元功能的static_assert

以下是我正在尝试做的事情的粗略摘要。

template<typename T>
struct fake_alloc {
    using value_type = T;
};
template<typename T, typename Alloc = fake_alloc<T>>
struct fake_cont {
    using value_type = T;
    // comment the line below out, and it compiles, how can I get it to compile without commenting this out???
    static_assert(std::is_same<value_type, typename Alloc::value_type>::value, "must be the same type");
};
template<typename T, typename U, typename = void>
struct sample_rebind {
    using type = T;
};
template<template<typename...> class Container, typename T, typename U, typename... OtherArgs>
struct sample_rebind<
    Container<T, OtherArgs...>,
    U,
    std::enable_if_t<
        std::is_constructible<
            Container<T, OtherArgs...>,
            Container<U, OtherArgs...>
        >::value
    >
>
{
    using type = Container<U, OtherArgs...>;
};
static_assert(
    std::is_same<
        fake_cont<int, fake_alloc<int>>,
        typename sample_rebind<fake_cont<int>, double>::type
    >::value,
    "This should pass!"
);

如您所见,所需的行为是最终static_assert应该通过,但不幸的是,它甚至没有达到这一点,因为当std::is_constructible<>尝试调用fake_cont的构造函数时,fake_cont中的static_assert被触发。

在真正的代码中fake_cont是libc++的std::vector,所以我无法修改它的胆量,或者std::is_constructible胆量。

任何

解决这个特定问题的建议都是值得赞赏的,特别感谢任何关于SFINAE围绕static_assert的建议。

编辑:is_same的第一部分应该被fake_cont<int, fake_alloc<int>>

编辑 2:如果在 fake_cont 中注释掉static_assert,它会编译 (clang 3.5(。这就是我想要的。所以我只需要一些方法来避免fake_cont static_assert.

namespace details {
  template<class T,class=void>
  struct extra_test_t: std::true_type {};
}

然后,我们将一个额外的测试放入:

template<class...>struct types{using type=types;};
template<template<typename...> class Container, typename T, typename U, typename... OtherArgs>
struct sample_rebind<
  Container<T, OtherArgs...>,
  U,
  std::enable_if_t<
    details::extra_test_t< types< Container<T, OtherArgs...>, U > >::value
    && std::is_constructible<
      Container<T, OtherArgs...>,
      Container<U, OtherArgs...>
    >::value
  >
> {
  using type = Container<U, OtherArgs...>;
};

我们编写额外的测试:

namespace details {
  template<class T, class Alloc, class U>
  struct extra_test_t<
    types<std::vector<T,Alloc>, U>,
    typename std::enable_if<
      std::is_same<value_type, typename Alloc::value_type>::value
    >::type
  > : std::true_type {};
  template<class T, class Alloc, class U>
  struct extra_test_t<
    types<std::vector<T,Alloc>, U>,
    typename std::enable_if<
      !std::is_same<value_type, typename Alloc::value_type>::value
    >::type
  > : std::false_type {};
}

基本上,这让我们可以在测试中注入"补丁"以匹配static_assert

如果我们有is_std_container<T>get_allocator<T>,我们可以写:

namespace details {
  template<template<class...>class Z,class T, class...Other, class U>
  struct extra_test_t<
    types<Z<T,Other...>>, U>,
    typename std::enable_if<
       is_std_container<Z<T,Other...>>>::value
       && std::is_same<
         value_type,
         typename get_allocator<Z<T,Other...>>::value_type
       >::value
    >::type
  > : std::true_type {};
  template<class T, class Alloc, class U>
  struct extra_test_t<
    types<std::vector<T,Alloc>, U>,
    typename std::enable_if<
       is_std_container<Z<T,Other...>>>::value
       && !std::is_same<
         value_type,
         typename get_allocator<Z<T,Other...>>::value_type
       >::value
    >::type
  > : std::false_type {};
}

或者我们可以说,任何有allocator_type的东西都可能无法反弹。

解决此问题

的容器感知方法是提取分配器类型(::allocator_type(,并将容器参数列表中分配器类型的所有实例替换为重新绑定 T 以某种方式U。 这仍然很棘手,因为std::map<int, int>有一个类型为 std::allocator< std::pair<const int, int> > 的分配器,并且无法以通用方式区分键int和值int

我已经设法获得了相当可靠的重新绑定初稿。它适用于所有 STL 容器(模板参数的不太常见的组合除外(、容器适配器和 std::integer_sequence 。它可能也适用于更多的事情。但它肯定不会适用于所有人。

主要的麻烦是让类似地图的类型像 Yakk 预测的那样工作,但一点类型特征对此有所帮助。

以此类推代码...

void_t

template<class...>
using void_t = void;

Walter E. Brown的这个小技巧使实现类型特征变得更加容易。

类型特征

template<class T, class = void>
struct is_map_like : std::false_type {};
template<template<class...> class C, class First, class Second, class... Others>
struct is_map_like<C<First, Second, Others...>,
                   std::enable_if_t<std::is_same<typename C<First, Second, Others...>::value_type::first_type,
                                                 std::add_const_t<First>>{} &&
                                    std::is_same<typename C<First, Second, Others...>::value_type::second_type,
                                                 Second>{}>>
    : std::true_type {};
template<class T, class U, class = void>
struct has_mem_rebind : std::false_type {};
template<class T, class U>
struct has_mem_rebind<T, U, void_t<typename T::template rebind<U>>> : std::true_type {};
template<class T>
struct is_template_instantiation : std::false_type {};
template<template<class...> class C, class... Others>
struct is_template_instantiation<C<Others...>> : std::true_type {};
  1. is_map_like利用了这样一个事实,即 STL 中的类映射类型都value_type定义为 a(n( std::pair,类映射类型的const第一个模板参数是pair中的first_type。类似地图类型的第二个模板参数与pairsecond_type完全匹配。 rebind必须更小心地处理类似地图的类型。
  2. has_mem_rebind 使用 void_t 技巧检测T上是否存在成员rebind元函数。如果一个类有rebind那么我们将首先遵循类的实现。
  3. is_template_instantiation检测类型T是否为模板实例化。这更多用于调试。

帮助程序类型列表

template<class... Types>
struct pack
{
    template<class T, class U>
    using replace = pack<
        std::conditional_t<
            std::is_same<Types, T>{},
            U,
            Types
        >...
    >;
    template<class T, class U>
    using replace_or_rebind = pack<
        std::conditional_t<
            std::is_same<Types, T>{},
            U,
            typename rebind<Types, U>::type
        >...
    >;
    template<class Not, class T, class U>
    using replace_or_rebind_if_not = pack<
        std::conditional_t<
            std::is_same<Types, Not>{},
            Types,
            std::conditional_t<
                std::is_same<Types, T>{},
                U,
                typename rebind<Types, U>::type
            >
        >...
    >;
    template<class T>
    using push_front = pack<T, Types...>;
};

这处理一些简单的列表,例如类型的操作

  1. replace以非递归方式将所有出现的T替换为U
  2. replace_or_rebindU 替换所有出现的T,对于所有不匹配的匹配项,调用重新绑定
  3. replace_or_rebind_if_notreplace_or_rebind相同,但跳过任何元素匹配Not
  4. push_front只是将一个元素推到类型列表的前面

调用成员重新绑定

// has member rebind implemented as alias
template<class T, class U, class = void>
struct do_mem_rebind
{
    using type = typename T::template rebind<U>;
};
// has member rebind implemented as rebind::other
template<class T, class U>
struct do_mem_rebind<T, U, void_t<typename T::template rebind<U>::other>>
{
    using type = typename T::template rebind<U>::other;
};
事实证明,根据

标准实现成员rebind有两种不同的有效方法。对于分配器来说,这是rebind<T>::other.对于指针来说,它只是rebind<T>.如果存在,则do_mem_rebind的这种实现与rebind<T>::other一起使用,否则它将回退到更简单的rebind<T>

打开

template<template<class...> class C, class Pack>
struct unpack;
template<template<class...> class C, class... Args>
struct unpack<C, pack<Args...>> { using type = C<Args...>; };
template<template<class...> class C, class Pack>
using unpack_t = typename unpack<C, Pack>::type;

这需要pack,提取它包含的类型,并将它们放入其他模板C中。

重新绑定实现

好东西。

template<class T, class U, bool = is_map_like<T>{}, bool = std::is_lvalue_reference<T>{}, bool = std::is_rvalue_reference<T>{}, bool = has_mem_rebind<T, U>{}>
struct rebind_impl
{
    static_assert(!is_template_instantiation<T>{}, "Sorry. Rebind is not completely implemented.");
    using type = T;
};
// map-like container
template<class U, template<class...> class C, class First, class Second, class... Others>
class rebind_impl<C<First, Second, Others...>, U, true, false, false, false>
{
    using container_type = C<First, Second, Others...>;
    using value_type = typename container_type::value_type;
    using old_alloc_type = typename container_type::allocator_type;
    using other_replaced = typename pack<Others...>::template replace_or_rebind_if_not<old_alloc_type, First, typename U::first_type>;
    using new_alloc_type = typename std::allocator_traits<old_alloc_type>::template rebind_alloc<std::pair<std::add_const_t<typename U::first_type>, typename U::second_type>>;
    using replaced = typename other_replaced::template replace<old_alloc_type, new_alloc_type>;
    using tail = typename replaced::template push_front<typename U::second_type>;
public:
    using type = unpack_t<C, typename tail::template push_front<typename U::first_type>>;
};
// has member rebind
template<class T, class U>
struct rebind_impl<T, U, false, false, false, true>
{
    using type = typename do_mem_rebind<T, U>::type;
};
// has nothing, try rebind anyway
template<template<class...> class C, class T, class U, class... Others>
class rebind_impl<C<T, Others...>, U, false, false, false, false>
{
    using tail = typename pack<Others...>::template replace_or_rebind<T, U>;
public:
    using type = unpack_t<C, typename tail::template push_front<U>>;
};
// has nothing, try rebind anyway, including casting NonType template parameters
template<class T, template<class, T...> class C, class U, T FirstNonType, T... Others>
struct rebind_impl<C<T, FirstNonType, Others...>, U, false, false, false, false>
{
    using type = C<U, U(FirstNonType), U(Others)...>;
};
// array takes a non-type parameter parameters
template<class T, class U, std::size_t Size>
struct rebind_impl<std::array<T, Size>, U, false, false, false, false>
{
    using type = std::array<U, Size>;
};
// pointer
template<class T, class U>
struct rebind_impl<T*, U, false, false, false, false>
{
    using type = typename std::pointer_traits<T*>::template rebind<U>;
};
// c-array
template<class T, std::size_t Size, class U>
struct rebind_impl<T[Size], U, false, false, false, false>
{
    using type = U[Size];
};
// c-array2
template<class T, class U>
struct rebind_impl<T[], U, false, false, false, false>
{
    using type = U[];
};
// lvalue ref
template<class T, class U>
struct rebind_impl<T, U, false, true, false, false>
{
    using type = std::add_lvalue_reference_t<std::remove_reference_t<U>>;
};
// rvalue ref
template<class T, class U>
struct rebind_impl<T, U, false, false, true, false>
{
    using type = std::add_rvalue_reference_t<std::remove_reference_t<U>>;
};
  1. rebind的失败情况是简单地保持类型不变。这允许呼叫rebind<Types, double>...,而不必担心Types中的每个Type是否rebind能够。里面有一个static_assert,以防它收到模板实例化。如果这被击中,您可能需要另一个专业化rebind
  2. 类似地图的rebind希望像rebind<std::map<int, int>, std::pair<double, std::string>>一样被调用。因此,分配器要回绑定到的类型与容器要重新绑定到的类型并不完全匹配。它对除键和值类型以外的所有类型执行replace_or_rebind_if_notif_notallocator_type。由于分配器类型与键/值对不同,因此rebind需要修改分配器对中第一个元素的const性。它使用 std::allocator_traits 重新绑定分配器,因为所有分配器都必须可通过 std::allocator_traits 重新绑定。
  3. 如果T有一个成员rebind,请使用它。
  4. 如果T没有成员rebind,则replace_or_rebind模板CC的第一个模板参数匹配的所有参数。
  5. 如果T有一个类型参数,以及一堆类型与该参数匹配的非类型模板参数。尝试将所有这些非类型参数重新转换为 U 。正是这种情况使std::integer_sequence起作用。
  6. std::array需要一个特殊情况,因为它需要一个非类型模板参数来提供它的大小,并且该模板参数应该单独保留。
  7. 这种情况允许将指针重新绑定到其他指针类型。它使用std::pointer_traitsrebind来实现这一点。
  8. 让我们rebind处理大小的 c 数组,例如:T[5]
  9. 让我们rebind在没有大小的 c 数组上工作,例如:T[]
  10. rebind s lvalue-ref
  11. T 类型到 a 保证 lvalue-ref 到 std::remove_reference_t<U> .
  12. rebind s 右值引用 T 类型到保证的 rvalue-ref 到 std::remove_reference_t<U>

派生(公开(类

template<class T, class U>
struct rebind : details::rebind_impl<T, U> {};
template<class T, class U>
using rebind_t = typename rebind<T, U>::type;

返回 SFINAE 和 static_assert

经过多次谷歌搜索,似乎没有像libc ++的STL容器那样static_assert s周围SFINAE的通用方法。这真的让我希望这门语言有一些对SFINAE更友好的东西,但比概念更特别一些。

喜欢:

template<class T>
    static_assert(CACHE_LINE_SIZE == 64, "")
struct my_struct { ... };