Search code examples
c++allocator

Use of std::allocator_traits<A>


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));
    }
    //...
};

Solution

  • 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.