How do I correctly create a container that works with both, C++11 and C++17 polymorphic allocators? Here's what I have so far (as a generic boilerplate template):
Explanation: I've included two fields, res_
which shows how dynamic memory is managed directly from the container, whereas field vec_
is used to demonstrate how the allocator propagates downwards. I've taken a lot from Pablo Halpern's talk Allocators: The Good Parts but he mainly talks about pmr allocators, not the c++11 ones.
#include <cstdio>
#include <vector>
#include <memory>
#include <memory_resource>
template <typename T, typename Allocator = std::allocator<T>>
struct MyContainer {
auto get_allocator() const -> Allocator {
return vec_.get_allocator();
}
MyContainer(Allocator allocator = {})
: vec_{ allocator }
{}
MyContainer(T val, Allocator allocator = {})
: MyContainer(allocator)
{
res_ = std::allocator_traits<Allocator>::allocate(allocator, sizeof(T));
std::allocator_traits<Allocator>::construct(allocator, res_, std::move(val));
}
~MyContainer() {
Allocator allocator = get_allocator();
std::allocator_traits<Allocator>::destroy(allocator, std::addressof(res_));
std::allocator_traits<Allocator>::deallocate(allocator, res_, sizeof(T));
res_ = nullptr;
}
MyContainer(const MyContainer& other, Allocator allocator = {})
: MyContainer(allocator)
{
operator=(other);
}
MyContainer(MyContainer&& other) noexcept
: MyContainer(other.get_allocator())
{
operator=(std::move(other));
}
MyContainer(MyContainer&& other, Allocator allocator = {})
: MyContainer(allocator)
{
operator=(std::move(other));
}
auto operator=(MyContainer&& other) -> MyContainer& {
if (other.get_allocator() == get_allocator()) {
std::swap(*this, other);
} else {
operator=(other); // Copy assign
}
}
auto operator=(const MyContainer& other) -> MyContainer& {
if (other != this) {
std::allocator_traits<Allocator>::construct(get_allocator(), std::addressof(vec_), vec_);
std::allocator_traits<Allocator>::construct(get_allocator(), std::addressof(res_), other);
}
return *this;
}
private:
std::vector<T, Allocator> vec_; // Propagation
T* res_ = nullptr;
};
int main() {
MyContainer<std::string, std::pmr::polymorphic_allocator<std::byte>> ctr1 = std::string{"Hello World!"};
MyContainer<double> ctr2 = 2.5;
}
However even this doesn't work as planned, as vector expects its value type to match that of the allocator:
<source>:67:31: required from 'struct MyContainer<std::__cxx11::basic_string<char>, std::pmr::polymorphic_allocator<std::byte> >'
<source>:72:74: required from here
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/stl_vector.h:438:64: error: static assertion failed: std::vector must have the same value_type as its allocator
438 | static_assert(is_same<typename _Alloc::value_type, _Tp>::value,
|
What else am I missing? Should I maybe propagate differently based on allocator's propagation traits (is this required for generic containers)?
value_type
that is the same as the value_type
of the container; otherwise it would be ill-formed.std::pmr::polymorphic_allocator<std::string>
for MyContainer
, or rebind the allocator type before passing it to std::vector
, e.g.:
// option 1
MyContainer<std::string, std::pmr::polymorphic_allocator<std::string>> ctr1 = /* ... */;
// option 2
template <class T, class Allocator = std::allocator<T>>
struct MyContainer {
// ...
private:
std::vector<T, typename std::allocator_traits<Allocator>::template rebind_alloc<T>> vec_;
};
MyContainer<std::string, std::pmr::polymorphic_allocator<std::byte>> ctr1 = /* ... */;
std::polymorphic_allocator
and std::allocator
is comparatively easy - both satisfy the named requirement Allocator, so the special sauce needed in this case is just to do nothing special - implement them as a bog-standard allocator (basically use std::allocator_traits<Alloc>
for all interactions with the allocator)
All containers that are allocator-aware containers must have an allocator with a value_type
that is the same as the value_type
of the container.
This is mandated in the standard by: (emphasis mine)
24.2.2.5 Allocator-aware containers (4)
(3) In this subclause,
(3.1) -X
denotes an allocator-aware container class with avalue_type
ofT
using an allocator of typeA
,
[...] A typeX
meets the allocator-aware container requirements ifX
meets the container requirements and the following types, statements, and expressions are well-formed and have the specified semantics.
typename X::allocator_type
- (4) Result:
A
- (5) Mandates:
allocator_type::value_type
is the same asX::value_type
.
So the following statement must always be true for an allocator-aware container:
static_assert(
std::same_as<
Container::value_type,
Container::allocator_type::value_type
>
);
Note that all containers defined in the standard library (except std::array
) are mandated to be allocator-aware. (see 24.2.2.5 (1) Allocator-aware containers)
Note that in your example that statement will not be satisfied:
// Hypothetical, won't compile
using Container = std::vector<std::string, std::pmr::polymorphic_allocator<std::byte>>;
// will be std::string
using ContainerValueType = Container::value_type;
// will be std::byte (std::pmr::polymorphic_allocator<std::byte>::value_type)
using AllocatorValueType = Container::allocator_type::value_type;
// would fail
static_assert(std::same_as<ContainerValueType, AllocatorValueType>);
std::vector
would not be an allocator-aware container (because it doesn't fulfill this requirement)std::vector
must be an allocator-aware container=> This is ill-formed due to contradiction in the standard.
Note that this also matches the error message you got from gcc:
error: static assertion failed: std::vector must have the same value_type as its allocator
The Youtube Video you linked in the comments (CppCon 2017: Pablo Halpern “Allocators: The Good Parts”) is about a user-defined container class that does not utilize any standard library containers.
There are no rules that the standard imposes for user-defined container types, so one can basically do whatever one wants there.
Here's a small transcript of the class the talk is about:
template<class Tp>
class slist {
public:
using value_type = Tp;
using reference = value_type&;
// ...
// non-template use of polymorphic_allocator
using allocator_type = std::pmr::polymorphic_allocator<std::byte>;
// Constructors
// Every constructor has an variant taking an allocator
slist(allocator_type a = {});
slist(const slist& other, allocator_type a = {});
slist(slist&& other);
slist(slist&& other, allocator_type a = {});
// ...
};
Note that the allocator_type
is hardcoded to std::pmr::polymorphic_allocator<std::byte>
, so allocator_type::value_type
will generally not match slist::value_type
(except the case where both are std::byte
);
So this container does not satisfy the requirements of an allocator-aware container most of the time.
But there's also no requirement for it to do so.
=> well-formed
Note: It would be ill-formed if one would pass e.g. an slist<>
to a function that mandates that its parameter must be an allocator-aware container. - But as long as one avoids that there's no issue with defining almost-conforming containers.
Note that std::pmr::polymorphic_allocator
satisfies the named requirement Allocator, exactly like std::allocator
does.
(All allocators that are intended to be used with standard containers must satisfy that requirement)
So the trick to support both is just to do nothing special - treat the std::pmr::polymorphic_allocator
like any other allocator, since it's just that. (use std::allocator_traits<Alloc>
for basically everything)
Note that this also means that you should respect the std::allocator_traits<Allocator>::propagate_on_
container_copy
/ container_move_assignment
/ container_swap
values.
Which for polymorphic_allocator
means that the allocator should not propagate when copying / moving / swapping the container.
Because doing so can lead to surprising lifetime issues - see for example this answer.
(Of course those should always be respected, not only just for polymorphic_allocator
s)