How to make static_assert play nice with SFINAE

2019-04-28 15:51发布

问题:

Update

I posted a working rough draft of rebind as an answer to the question. Though I didn't have much luck finding a generic way to keep static_asserts from breaking metafunctions.


Basically I want to check if a templated type T<U, Args...> can be constructed from some other type T<V, Args...>. Where T and Args... is the same in both types. The problem is, T<> might have a static_assert in it that totally breaks my metafunction.

Below is a rough summary of what I'm trying to do.

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

As you can see the desired behavior is that the final static_assert should pass, but unfortunately, it doesn't even get to that point as the static_assert in fake_cont is triggered when std::is_constructible<> attempts to call fake_cont's constructor.

In the real code fake_cont is libc++'s std::vector, so I can't modify it's guts, or std::is_constructible's guts.

Any advice for working around this specific issue is appreciated, and any advice in general for SFINAE'ing around static_assert's is especially appreciated.

Edit: the first part of the is_same should have been fake_cont<int, fake_alloc<int>>

Edit 2: If you comment out the static_assert in fake_cont, it compiles (clang 3.5). And that's what I want. So I just need some way to avoid the static_assert in fake_cont.

回答1:

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

We then fold an extra test in:

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

and we write the extra test:

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

basically, this lets us inject "patches" on our test to match the static_assert.

If we had is_std_container<T> and get_allocator<T>, we could write:

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

or we could just state that anything with an allocator_type probably cannot be rebound.

A more container-aware approach to this problem would be to extract the allocator type (::allocator_type), and replace all instances of the allocator type in the container argument list with a rebind of T to U somehow. This is still tricky, as std::map<int, int> has an allocator of type std::allocator< std::pair<const int, int> >, and distinguishing between the key int and the value int isn't possible in a generic way.



回答2:

I've managed to get a pretty solid first draft of rebind going. It works for all the STL containers (barring less common combinations of template parameters), the container adapters, and std::integer_sequence. And it probably works for a lot more things as well. But it certainly won't work for everything.

The main trouble was getting the map-like types to work as Yakk predicted, but a little type trait helped out with that.

So on to the code...

void_t

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

This little trick by Walter E. Brown makes implementing type traits a lot easier.

Type Traits

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 uses the fact that the map-like types in the STL all have value_type defined to be a(n) std::pair with the consted first template parameter of the map-like type, being the first_type in the pair. The second template parameter of the map-like type matches exactly the pair's second_type. rebind has to handle map-like types more carefully.
  2. has_mem_rebind is detects the presence of a member rebind meta-function on T using the void_t trick. If a class has rebind then we'll defer to the classes implementation first.
  3. is_template_instantiation detects if the type T is a template instantiation. This is more for debugging.

Helper Type List

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

This handles some simple list like manipulations of types

  1. replace replaces all occurrences of T with U in a non-recursive fashion.
  2. replace_or_rebind replaces all occurrences of T with U, and for all non-matching occurrences, calls rebind
  3. replace_or_rebind_if_not is the same as replace_or_rebind but skips over any element matching Not
  4. push_front simply pushes an element on to the front of the type-list

Calling Member Rebind

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

It turns out there's two different valid ways to implement a member rebind according to the standard. For allocators it's rebind<T>::other. For pointers it's just rebind<T>. This implementation of do_mem_rebind goes with rebind<T>::other if it exists, otherwise it falls back to the simpler rebind<T>.

Unpacking

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;

This takes a pack, extracts the types it contains, and puts them into some other template C.

Rebind Implementation

The good stuff.

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. The fail case for rebind is to simply leave the type unchanged. This allows calling rebind<Types, double>... without having to worry about whether every Type in Types is rebindable. There's a static_assert in there in case it receives a template instantiation. If that's hit, you probably need another specialization of rebind
  2. The map-like rebind expects to be invoked like rebind<std::map<int, int>, std::pair<double, std::string>>. So the type the allocator is being rebound to, doesn't exactly match the type the container is being rebound to. It does a replace_or_rebind_if_not on all the types except the Key and Value types, with the if_not being the allocator_type. Since the allocator type differs from the key/value pair rebind needs to modify the constness of the first element of the pair. It uses std::allocator_traits to rebind the allocator, as all allocators must be rebindable via std::allocator_traits.
  3. If T has a member rebind, use that.
  4. If T has no member rebind, replace_or_rebind all parameters to the template C that match C's first template parameter.
  5. If T has one type parameter, and a bunch of non-type template parameters whose type matches that parameter. Attempt to recast all of those non-type parameters to U. This is the case that makes std::integer_sequence work.
  6. A special case was required for std::array as it takes a non-type template parameter giving it's size, and that template parameter should be left alone.
  7. This case allows for rebinding of pointers to other pointer types. It uses std::pointer_traits's rebind to accomplish this.
  8. Lets rebind work on sized c-arrays ex: T[5]
  9. Lets rebind work on c-arrays without a size ex: T[]
  10. rebinds lvalue-ref T types to a guaranteed lvalue-ref to std::remove_reference_t<U>.
  11. rebinds rvalue-ref T types to a guaranteed rvalue-ref to std::remove_reference_t<U>.

Derived (Exposed) Class

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;

Back To SFINAE and static_assert

After much googling there doesn't seem to be a generic way to SFINAE around static_asserts like the ones in libc++'s STL containers. It really makes me wish the language had something more SFINAE friendly, but a little more ad-hoc than concepts.

Like:

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