5
\$\begingroup\$

I wrote an STL allocator which uses boost::pool to allocate the memory for std::vector container.

This is my first time I implement an allocator and while working on this class I found that STL containers (std::vector for example) copy, destruct, create many times the assigned allocator during their lifetime. For example, during pushing operation into std::vector, the allocator's copy constructor and destructor are called several times. Due to this I also had to wrap the boost::pool with a smart pointer because when I was passing the pool as external argument and then destroying it explicitly I was getting crashes on program exist inside the allocator because some clones of it were still trying to access the pool. I am still not sure I completely understand the mechanics of how STL containers treat allocators during their lifetime. Therefore I am asking to review this class implementation.

Also I would like to have the answer to the following questions:

  1. Is this the right way to use boost::pool for raw memory allocation? My intention was to allocate a large pool (in this example it is just 4096 bytes) of single byte blocks to mimic raw heap memory arena, because the allocator interface requests the memory in varying sizes multiplied by the data size.

  2. If the answer to the question above is positive, can I share the same allocator instance between several std::vector instances (with the same data type)? Besides thread unsafety, what are the possible caveats or pitfalls I need to take into account?

template<typename T>
class Allocator{

friend class Allocator;
private:
  typedef std::shared_ptr<boost::pool<> >  PoolSP;
  PoolSP m_pool;

public:

typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
typedef T value_type;


/// Default constructor
Allocator()  throw()
{
   m_allocUsedSize = 0;
   m_pool.reset(new boost::pool<>(sizeof(uint8_t), 4096));

}
/// Copy constructor
Allocator(const Allocator& other) throw()
{
    *this = other;
}
/// Destructor
~Allocator()
{
}
/// Copy constructor with another type
template<typename U>
Allocator(const Allocator<U> &other) throw()
{
    *this = other;
}
/// Copy
Allocator<T>& operator=(const Allocator& other)
{
    m_pool = other.m_pool;
    return *this;
}


/// Copy with another type
template<typename U>
Allocator& operator=(const Allocator<U>& other)
{
    m_pool = other.m_pool;
    return *this;
}

/// Get address of a reference
pointer address(reference x) const
{
    return &x;
}

/// Get const address of a const reference
const_pointer address(const_reference x) const
{
    return &x;
}

/// Allocate n elements of type T
pointer allocate(size_type n, const void*  = 0)
{
    size_type size = n * sizeof(value_type);
    m_allocUsedSize += size;
    return  (pointer)m_pool->ordered_malloc(size); //
}

/// Free memory of pointer p
void deallocate(void* p, size_type n)
{
    size_type size = n * sizeof(value_type);
    m_allocUsedSize -= size;
    m_pool->ordered_free(p);
}

/// Call the constructor of p
void construct(pointer p, const T& val)
{
// Placement new
    new ((T*)p) T(val);
}

/// Call the constructor of p with many arguments. C++11
template<typename U, typename... Args>
void construct(U* p, Args&&... args)
{
    // Placement new
    ::new((void*)p) U(std::forward<Args>(args)...);
}

/// Call the destructor of p
void destroy(pointer p)
{
    p->~T();
}
/// Call the destructor of p of type U
template<typename U>
void destroy(U* p)
{
    p->~U();
}
void reset()
{
   m_pool->purge_memory();
   m_pool.reset();
}

/// A struct to rebind the allocator to another allocator of type U
template<typename U>
struct rebind
{
    typedef  Allocator<U> other;
};
};

size_t m_allocUsedSize;

This implementation works and is in fact almost twice faster than using the default std::vector allocator. But memory errors are nasty things. That's one of the reasons why I want to see an input from more people.

\$\endgroup\$
  • \$\begingroup\$ Just a note about your previous allocator: allocators in C++11 must be stateless, read this: stackoverflow.com/questions/12545072/… \$\endgroup\$ – firda Aug 17 '15 at 20:01
  • \$\begingroup\$ @fida you mean I need to externalize the boost::pool instance? \$\endgroup\$ – Michael IV Aug 17 '15 at 20:05
4
\$\begingroup\$

Stateless C++11 allocators

Allocators were at first (my opinion) designed to be simple empty classes / templates, to just call malloc or similar function. They are treated this way as if copying was no-cost. I would personally design it in a bit different way, but I understand that it is what it is.

Allocator must therefore be either empty (with only static/shared members) or a reference/pointer to the real logic. Using shared_pointer is therefore fine and satisfy the stateless requirement as I understand it (it handles the destruction as well).

boost::pool as allocator

This is actually something I have used myself for tiny blocks. It can help a lot if your application needs to allocate many tiny objects (2-3 pointers/integers per object). But beware...

used in std::vector

But vector is not so good with such an allocator, tree would be. The problem is that vector is an array, which reallocates as needed. That often means that new array is created, items copyed and old array destroyed. Container that does not reallocate its node would be better (e.g. tree or deque). In better words: boost::pool would better be utilized with such "no-destroy unless deleting" container.

using single allocator in multiple containers

Yes, of course you can. But take care about thread-safety and copy/move constructors (which you solved by using shared_pointer).

the code

Well, your indentation may need some improvements, your last line looks like you have the counter outside of the class. But other than that, I can see no errors.

\$\endgroup\$
  • \$\begingroup\$ P.S.: vector+pool may be a bit faster at first, because you are kinda pre-allocating the vector and changing its reallocation strategy a bit, but it could as well be worse in some scenairo. Better profiling/benchmarking needed. \$\endgroup\$ – firda Aug 17 '15 at 20:30
  • \$\begingroup\$ I have the counter variable outside because it is declared as "extern" .And if I don't initialize it after class declaration I am getting linking errors. \$\endgroup\$ – Michael IV Aug 18 '15 at 8:02
  • \$\begingroup\$ See this for static template-member initialization. This way you share the counter for all the allocators. And indentation or at least some good separating comment (//======) would be fine to add. \$\endgroup\$ – firda Aug 18 '15 at 10:00
  • \$\begingroup\$ Yeah,comments and indents are on TODO,the work is still in progress. \$\endgroup\$ – Michael IV Aug 18 '15 at 10:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.