Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the equivalent of new and delete using allocators?

Tags:

c++

allocator

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

like image 657
Ben Avatar asked Oct 16 '25 20:10

Ben


1 Answers

My core question is: if allocators are intended to replace new and delete, why do they only provide an API like malloc and free?

The API of allocators are such as it is because an important point of allocators is that memory allocation and object creation must be separated. This is necessary for example to implement a container such as std::vector. Allocators are a generalisation of operator new / delete, not generalisation of the new expression.

is this really not provided by the standard library?

No, these functions aren't provided by the standard library.

Is this the right way to implement essentially new and delete and make_unique using allocators?

auto dtor = [&alloc](T* ptr) { destruct_and_deallocate(alloc, ptr); };
             ^

Capturing allocator by reference into the deleter seems bad. It should be copied to be safe.

Other suggestions:

  • In the default case of std::allocator, we would like to avoid paying for the overhead of the deleter. Consider adding a specialisation that delegates to std::make_unique when std::allocator is used.

  • You could avoid the try-catch by using an intermediary unique pointer with deleter that only deallocates:

    T* ptr = traits_t::allocate(rebound_alloc, 1);
    auto dealloc = [&](T* ptr) { traits_t::deallocate(rebound_alloc, ptr, 1); };
    std::unique_ptr<T, decltype(dealloc)> storage(ptr, dealloc);
    traits_t::construct(rebound_alloc, storage.get(), std::forward<Args>(args)...);
    auto dtor = [alloc](T* ptr) { destruct_and_deallocate(alloc, ptr); };
    return std::unique_ptr<T, decltype(dtor)>(storage.release(), dtor);
    

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?

No, object's have no knowledge of the allocator that creates them or allocates their memory. "Allocator aware" containers are simply generic tempaltes that allow the user to provide a custom allocator, and avoid allocating memory through other means.

like image 99
eerorika Avatar answered Oct 18 '25 12:10

eerorika



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!