Why not to inherit from std::allocator

I created my own allocator like so:

template<typename T>
class BasicAllocator
{
    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;


        BasicAllocator() throw() {};
        BasicAllocator (const BasicAllocator& other) throw() {};

        template<typename U>
        BasicAllocator (const BasicAllocator<U>& other) throw() {};

        template<typename U>
        BasicAllocator& operator = (const BasicAllocator<U>& other) {return *this;}
        BasicAllocator<T>& operator = (const BasicAllocator& other) {return *this;}
        ~BasicAllocator() {}

        pointer address (reference value) const {return &value;}
        const_pointer address (const_reference value) const {return &value;}

        pointer allocate (size_type n, const void* hint = 0) {return static_cast<pointer> (::operator new (n * sizeof (value_type) ) );}
        void deallocate (void* ptr, size_type n) {::operator delete (static_cast<T*> (ptr) );}

        template<typename U, typename... Args>
        void construct (U* ptr, Args&&  ... args) {::new (static_cast<void*> (ptr) ) U (std::forward<Args> (args)...);}
        void construct (pointer ptr, const T& val) {new (static_cast<T*> (ptr) ) T (val);}

        template<typename U>
        void destroy (U* ptr) {ptr->~U();}
        void destroy (pointer ptr) {ptr->~T();}

        size_type max_size() const {return std::numeric_limits<std::size_t>::max() / sizeof (T);} /**return std::size_t(-1);**/

        template<typename U>
        struct rebind
        {
            typedef BasicAllocator<U> other;
        };
};

But I want to know why I should never inherit from std::allocator. Is it because it doesn't have a virtual destructor? I've seen many posts saying that one should create their own and not inherit. I understand why we shouldn't inherit std::string and std::vector but what is wrong with inheriting std::allocator?

Can I inherit my class above? Or do I need a virtual destructor to do that?

Why?


Solution 1:

A lot of people are going to post in this thread that you should not inherit from std::allocator because it doesn't have a virtual destructor. They'll talk about polymorphism and slicing and deleting via pointer-to-base class, none of which are permitted by the allocator requirements as detailed in section 17.6.3.5 [allocator.requirements] of the standard. Until someone demonstrates that a class derived from std::allocator fails to meet one of those requirements, it's simple cargo cult mentality.

That said, there is little reason to derive from std::allocator in C++11. C++11's overhaul of allocators introduced the traits template std::allocator_traits to sit between an allocator and its users and provide reasonable defaults for many of the required features via template metaprogramming. A minimal allocator in C++11 can be as simple as:

template <typename T>
struct mallocator {
  using value_type = T;

  mallocator() = default;
  template <class U>
  mallocator(const mallocator<U>&) {}

  T* allocate(std::size_t n) {
    std::cout << "allocate(" << n << ") = ";
    if (n <= std::numeric_limits<std::size_t>::max() / sizeof(T)) {
      if (auto ptr = std::malloc(n * sizeof(T))) {
        return static_cast<T*>(ptr);
      }
    }
    throw std::bad_alloc();
  }
  void deallocate(T* ptr, std::size_t n) {
    std::free(ptr);
  }
};

template <typename T, typename U>
inline bool operator == (const mallocator<T>&, const mallocator<U>&) {
  return true;
}

template <typename T, typename U>
inline bool operator != (const mallocator<T>& a, const mallocator<U>& b) {
  return !(a == b);
}

EDIT: Proper use of std::allocator_traits isn't fully present in all standard libraries yet. For example, the sample allocator above doesn't work correctly with std::list when compiled with GCC 4.8.1 - the std::list code complains about missing members since it hasn't been updated yet.

Solution 2:

The class template std::allocator<...> doesn't have any virtual functions. Thus, it is clearly a bad candidate to provide derived functionality. While some classes or class templates are still reasonable base classes, even without a virtual destructor and any other virtual function, these tend to be either just tag types or use the Curiously recurring template pattern.

Allocators are not intended to be customized like that, i.e., std::allocator<T> isn't intended as a base class. If you tried to use it as such, your logic may easily end up being sliced off. The approach used for easy customization of allocators is to rely on std::allocator_traits<A> to provide the various operations your allocator choose not to provide explicitly using a default implementation based on a relatively small number of operations.

The main issue about deriving from std::allocator<T> is that it may hide a problem with the rebind member, e.g., the member being omitted or misspelled. Below is an example which should print my_allocator::allocate() twice but doesn't due to a typo. I think my_allocator<T> is except for the typo a complete allocator even without the inheritance from std::allocator<T>, i.e., the unnecessary inheritance only contributes to the potential to hiding errors. You can also get an error, e.g., by getting the allocate() or deallocate() function wrong.

#include <memory>
#include <iostream>

template <typename T>
struct my_allocator
    : std::allocator<T>
{
    my_allocator() {}
    template <typename U> my_allocator(my_allocator<U> const&) {}

    typedef T value_type;
    template <typename U> struct rebimd { typedef my_allocator<U> other; };
    T* allocate(size_t n) {
        std::cout << "my_allocator::allocate()\n";
        return static_cast<T*>(operator new(n*sizeof(T)));
    }
    void deallocate(T* p, size_t) { operator delete(p); }
};

template <typename A>
void f(A a)
{
    typedef std::allocator_traits<A>    traits;
    typedef typename traits::value_type value_type;
    typedef typename traits::pointer    pointer;
    pointer p = traits::allocate(a, sizeof(value_type));
    traits::deallocate(a, p, sizeof(value_type));

    typedef typename traits::template rebind_alloc<int> other;
    typedef std::allocator_traits<other> otraits;
    typedef typename otraits::value_type ovalue_type;
    typedef typename otraits::pointer    opointer;
    other o(a);
    opointer op = otraits::allocate(o, sizeof(ovalue_type));
    otraits::deallocate(o, op, sizeof(ovalue_type));
}

int main()
{
    f(my_allocator<int>());
}