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)?
tl;dr
value_typethat is the same as thevalue_typeof the container; otherwise it would be ill-formed.So in this case one would need to either use a
std::pmr::polymorphic_allocator<std::string>forMyContainer, or rebind the allocator type before passing it tostd::vector, e.g.:std::polymorphic_allocatorandstd::allocatoris 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 usestd::allocator_traits<Alloc>for all interactions with the allocator)1. Why the given code example is ill-formed
All containers that are allocator-aware containers must have an allocator with a
value_typethat is the same as thevalue_typeof the container.This is mandated in the standard by: (emphasis mine)
So the following statement must always be true for an allocator-aware container:
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:
std::vectorwould not be an allocator-aware container (because it doesn't fulfill this requirement)std::vectormust 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:
2. Why it's not a problem in the linked video
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:
Note that the
allocator_typeis hardcoded tostd::pmr::polymorphic_allocator<std::byte>, soallocator_type::value_typewill generally not matchslist::value_type(except the case where both arestd::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.3. How to write a container that works with any allocator
Note that
std::pmr::polymorphic_allocatorsatisfies the named requirement Allocator, exactly likestd::allocatordoes.(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_allocatorlike any other allocator, since it's just that. (usestd::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_swapvalues.Which for
polymorphic_allocatormeans 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_allocators)