【问题标题】:How to make static_assert play nice with SFINAE如何使 static_assert 与 SFINAE 配合得很好
【发布时间】:2015-05-27 07:16:45
【问题描述】:

更新

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


基本上我想检查是否可以从其他类型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 应该通过,但不幸的是,它甚至没有达到这一点,因为fake_cont 中的static_assertstd::is_constructible&lt;&gt; 尝试调用时被触发fake_cont 的构造函数。

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

感谢任何有关解决此特定问题的建议,特别感谢有关 SFINAE 围绕static_assert 的任何建议。

编辑:is_same 的第一部分应该是fake_cont&lt;int, fake_alloc&lt;int&gt;&gt;

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

【问题讨论】:

  • 您的问题不清楚,因为您提供的示例永远不会起作用。你的意思是sample_rebind&lt;fake_cont&lt;double&gt;, int&gt;?如果您保留OtherArgs...,即使这样也会导致fake_cont&lt;int,fake_alloc&lt;double&gt;&gt;。就目前而言,编译器拒绝您的代码是正确的。
  • 我在fake_cont 中注释掉了static_assert,它确实可以编译(至少在clang 3.5 上),传递了最终的static_assert。我并不是说编译器是错误的,我只是在问如何使编译器不会在专业化的 is_constructible 部分失败。请注意,如果类型不可构造,sample_rebind 返回原始类型
  • 它通过了,因为您在非专业情况下使用了using type = T;(当时采用了这种情况)。您只是结合了三个错误以使其通过!删除其中任何一个都会失败。
  • 我知道,这正是我想要的。忽略你对sample_rebind 的预期行为是......它只是为了展示我想通过的测试......或者真的失败。我只想能够避免fake_cont中的static_assert

标签: c++ c++14 template-meta-programming sfinae static-assert


【解决方案1】:
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&lt;T&gt;get_allocator&lt;T&gt;,我们可以这样写:

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),并以某种方式将容器参数列表中的分配器类型的所有实例替换为 TU 的重新绑定.这仍然很棘手,因为std::map&lt;int, int&gt; 有一个std::allocator&lt; std::pair&lt;const int, int&gt; &gt; 类型的分配器,并且无法以通用方式区分键int 和值int

【讨论】:

  • 关于替换 ::allocator_type 的评论是个好主意,而且您对 map 的警告似乎可以解决(通过专注于 allocator_template&lt;std::pair/tuple&lt;...&gt;&gt;)。但是std::vector&lt;std::pair&lt;...&gt;&gt; 会打破这一点。这个周末我会努力工作的。
【解决方案2】:

我已经设法获得了一个相当可靠的重新绑定初稿。它适用于所有 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 定义为一个(n)std::pair,其中 consted 类映射类型的第一个模板参数是 @987654330 @在pair。 map-like 类型的第二个模板参数与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_rebind 将所有出现的T 替换为U,对于所有不匹配的出现,调用重新绑定
  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 有两种不同的有效方法。对于allocators,它是rebind&lt;T&gt;::other。对于pointers,它只是rebind&lt;T&gt;do_mem_rebind 的这个实现与 rebind&lt;T&gt;::other 如果存在,否则将回退到更简单的 rebind&lt;T&gt;

拆包

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&lt;Types, double&gt;...,而不必担心Types 中的每个Type 是否为rebindable。那里有一个static_assert,以防它接收到模板实例化。如果遇到这种情况,您可能需要rebind 的另一个专业化
  2. 类似于地图的rebind 期望像rebind&lt;std::map&lt;int, int&gt;, std::pair&lt;double, std::string&gt;&gt; 一样被调用。所以分配器被反弹到的类型与容器被反弹到的类型不完全匹配。它对除键和值类型之外的所有类型执行replace_or_rebind_if_notif_notallocator_type。由于分配器类型与键/值对不同,rebind 需要修改该对的第一个元素的constness。它使用std::allocator_traits 重新绑定分配器,因为所有分配器都必须可以通过std::allocator_traits 重新绑定。
  3. 如果T 有一个成员rebind,请使用它。
  4. 如果T 没有成员rebind,则replace_or_rebind 模板C 中与C 的第一个模板参数匹配的所有参数。
  5. 如果T 有一个类型参数,以及一堆类型与该参数匹配的非类型模板参数。尝试将所有这些非类型参数重铸为U。这就是 std::integer_sequence 工作的原因。
  6. std::array 需要一个特殊情况,因为它需要一个非类型模板参数来给出它的大小,并且该模板参数应该单独保留。
  7. 这种情况允许将指针重新绑定到其他指针类型。它使用std::pointer_traitsrebind 来完成此操作。
  8. rebind 处理大小的 c 数组,例如:T[5]
  9. rebind 处理没有大小的 c 数组 例如:T[]
  10. rebinds lvalue-ref T 类型为保证 lvalue-ref 到 std::remove_reference_t&lt;U&gt;
  11. rebinds rvalue-ref T 类型为保证 rvalue-ref 到 std::remove_reference_t&lt;U&gt;

派生(公开)类

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_asserts 周围找到 SFINAE 的通用方法。这真的让我希望该语言有一些对 SFINAE 更友好,但比概念更临时的东西。

喜欢:

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

【讨论】:

    猜你喜欢
    • 2014-04-07
    • 2019-05-18
    • 1970-01-01
    • 2011-10-24
    • 2016-08-18
    • 1970-01-01
    • 1970-01-01
    • 2010-09-11
    • 1970-01-01
    相关资源
    最近更新 更多