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 {};
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 const
ed 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.
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.
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
replace
replaces all occurrences of T
with U
in a non-recursive fashion.
replace_or_rebind
replaces all occurrences of T
with U
, and for all non-matching occurrences, calls rebind
replace_or_rebind_if_not
is the same as replace_or_rebind
but skips over any element matching Not
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>>;
};
- 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 rebind
able. 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
- 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 const
ness 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
.
- If
T
has a member rebind
, use that.
- If
T
has no member rebind
, replace_or_rebind
all parameters to the template C
that match C
's first template parameter.
- 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.
- 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.
- This case allows for rebinding of pointers to other pointer types. It uses
std::pointer_traits
's rebind
to accomplish this.
- Lets
rebind
work on sized c-arrays ex: T[5]
- Lets
rebind
work on c-arrays without a size ex: T[]
rebind
s lvalue-ref T
types to a guaranteed lvalue-ref to std::remove_reference_t<U>
.
rebind
s 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_assert
s 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 { ... };
sample_rebind<fake_cont<double>, int>
? And even that would result infake_cont<int,fake_alloc<double>>
if you keepOtherArgs...
. As it stands currently, the compiler is simply correct to reject your code. – Unthronestatic_assert
infake_cont
, and it does indeed compile (atleast on clang 3.5), passing the final static_assert. I'm not saying the compiler is wrong, I'm just asking how I can make the compiler not fail on theis_constructible
part of the specialization. Note that if the type is not constuctiblesample_rebind
returns the original type – Primerusing type = T;
in the non-specialized case (which it is taken then). You just combined three errors to make it pass! Remove any of them and it will fail. – Unthronesample_rebind
is... it's just there to demonstrate a test I want to pass... or fail really. I just want to be able to avoid thestatic_assert
infake_cont
– Primer