Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use of std::allocator_traits<A>

Tags:

c++

allocator

I would like to design a class template which takes an allocator type (as defined in Standard section 17.6.3.5) as a template argument. I see how std::allocator_traits<A> helpfully fills in any missing members of A with default settings. Beyond that, is there anything in the Standard Library or boost that would help use the allocator correctly?

In particular:

  1. To honor the typedefs like std::allocator_traits<A>::propagate_on_container_copy_assignment, do I have to check these things in the special member functions of each class which has a member of type A? Or is there some wrapper type I could use as a member instead which would take care of this stuff?

  2. If I want to overallocate to reduce the number of allocations by storing extra data next to the user-visible objects, is it appropriate to rebind the allocator something like this?

.

template<typename T, typename A>
class MyClass
{
private:
    //...
    struct storage {
        int m_special_data;
        T m_obj;
    };
    typedef typename std::allocator_traits<A>::template rebind_alloc<storage>
        storage_alloc;
    typedef typename std::allocator_traits<A>::template rebind_traits<storage>
        storage_traits;
    storage_alloc m_alloc;

    static T* alloc(T&& obj)
    {
        storage_traits::pointer sp = storage_traits::allocate(m_alloc, 1);
        sp->m_special_data = 69105;
        return ::new(&sp->m_obj) T(std::move(obj));
    }
    //...
};
like image 786
aschepler Avatar asked Mar 13 '12 17:03

aschepler


1 Answers

I don't know of anything to make life easier, allocator_traits really makes it simpler to write an allocator, by providing all the boilerplate code, but it doesn't help use an allocator.

So that I could use a single allocator API in both C++03 and C++11 code I added <ext/alloc_traits.h> to GCC 4.7, the class template __gnu_cxx::__alloc_traits provides a consistent API that uses allocator_traits in C++11 mode and calls the relevant member functions directly on the allocator in C++03 mode.

  1. No, there's no wrapper or shortcut, the C++11 allocator requirements make a container author's job much more complicated. The requirements are slightly different for each container, depending on how it manages memory. For a vector-like type, in the copy-assignment operator if propagate_on_container_copy_assignment (POCCA) is false and the existing capacity is greater than the source object's size then you can re-use the existing memory (if POCCA is true and the new allocator is not equal you can't re-use the old memory as it won't be possible to de-allocate it later after the allocator is replaced) but that optimization doesn't help much for a node-based container such as list or map.

  2. That looks almost right, although you probably want to replace

    return ::new(&sp->m_obj) T(std::move(obj));
    

    with

    A a(m_alloc);    
    std::allocator_traits<A>::construct(a, &sp->m_obj, std::move(obj));
    return &sp->m_obj;
    

As stated in [container.requirements.general]/3 the containers which use an allocator use allocator_traits<A>::construct to create the element type T itself but any other types allocated (such as your storage) must not use construct.

If storage itself is constructed then it will construct storage::m_obj unless that member is a type which can be left uninitialized, such as std::aligned_storage<sizeof(T)>, that can be initialized explicitly later by allocator_traits<A>::construct. Alternatively, individually construct each member that needs non-trivial construction e.g. if storage also had a string member:

    storage_traits::pointer sp = storage_traits::allocate(m_alloc, 1);
    sp->m_special_data = 69105;
    ::new (&sp->m_str) std::string("foobar");
    A a(m_alloc);    
    std::allocator_traits<A>::construct(a, &sp->m_obj, std::move(obj));
    return &sp->m_obj;

The m_special_data member is a trivial type so its lifetime begins as soon as storage is allocated for it. The m_str and m_obj members need non-trivial initialization so their lifetimes begin when their constructors complete, which is done by the placement new and the construct call, respectively.

Edit: I've recently learnt that the standard has a defect (which I've reported) and the calls to construct do not need to use a rebound allocator, so these lines:

    A a(m_alloc);    
    std::allocator_traits<A>::construct(a, &sp->m_obj, std::move(obj));

can be replaced with:

    std::allocator_traits<storage_alloc>::construct(m_alloc, &sp->m_obj, std::move(obj));

Which makes life slightly easier.

like image 153
Jonathan Wakely Avatar answered Oct 30 '22 14:10

Jonathan Wakely