C++ allocators (as used by std::vector) are tricky to. I understand that they changed a lot to allow stateful allocators and PMR, leading to some of the cruftiness. My core question is: if allocators are intended to replace new and delete, why do they only provide an API like malloc and free? I understand why, e.g., std::vector needs a malloc interface since it needs to allocate a buffer without calling constructors, but in general, it seems like we are missing these functions:
#include <cassert>
#include <iostream>
#include <memory>
//! Rebind alloc to type T
template <typename T, typename Alloc>
auto rebound_allocator(const Alloc& alloc) {
return typename std::allocator_traits<Alloc>::template rebind_alloc<T>{alloc};
}
//! Like operator delete but for a single T allocated by rebound_allocator<T>(alloc).
template <typename T, typename Alloc>
void allocator_delete(const Alloc& alloc, T* ptr) {
assert(ptr);
auto a = rebound_allocator<T>(alloc);
using traits_t = std::allocator_traits<decltype(a)>;
// Should we try/catch around destroy and always deallocate?
traits_t::destroy(a, ptr);
traits_t::deallocate(a, ptr, 1);
}
//! Returned memory must be freed with, e.g., allocator_delete(alloc, ptr).
template <typename T, typename Alloc, typename... Args>
[[nodiscard]] T* allocator_new(const Alloc& alloc, Args&&... args) {
auto a = rebound_allocator<T>(alloc);
using traits_t = std::allocator_traits<decltype(a)>;
auto deallocate = [&a](T* ptr) { traits_t::deallocate(a, ptr, 1); };
// Hold in a unique_ptr to deallocate if construction throws.
auto buf = std::unique_ptr<T, decltype(deallocate)>(traits_t::allocate(a, 1), deallocate);
traits_t::construct(a, buf.get(), std::forward<Args>(args)...);
return buf.release();
}
//! Like make_unique. Beware: The allocator is is referenced by the deleter!
template <typename T, typename Alloc, typename... Args>
[[nodiscard]] auto allocator_make_unique(const Alloc& alloc, Args&&... args) {
auto dtor = [&alloc](T* ptr) { allocator_delete<T>(alloc, ptr); };
return std::unique_ptr<T, decltype(dtor)>(allocator_new<T>(alloc, std::forward<Args>(args)...),
dtor);
}
struct S {
float x;
S(float x) : x(x) { std::cout << "S::S()" << std::endl; }
~S() { std::cout << "S::~S()" << std::endl; }
};
int main() {
std::allocator<int> alloc;
auto ptr = allocator_make_unique<S>(alloc, 42.5f);
assert(ptr);
std::cout << ptr->x << std::endl;
}
Output:
S::S()
42.5
S::~S()
https://godbolt.org/z/sheec6br3
Am I missing something? Is this the right way to implement essentially new and delete and make_unique using allocators? If so, is this really not provided by the standard library?
Edit:
I think (but am not sure?) that if T is allocator-aware, traits_t::construct(a, ptr, n) will propagate itself into the created object?
Edit: Here's a cleaned-up version: https://godbolt.org/z/47Tdzf4W7
Edit: Original version: https://godbolt.org/z/dGW7hzdc1