Why Not to Inherit from Std::Allocator

Why not to inherit from std::allocator

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.

Thou shalt not inherit from std::vector

Actually, there is nothing wrong with public inheritance of std::vector. If you need this, just do that.

I would suggest doing that only if it is really necessary. Only if you can't do what you want with free functions (e.g. should keep some state).

The problem is that MyVector is a new entity. It means a new C++ developer should know what the hell it is before using it. What's the difference between std::vector and MyVector? Which one is better to use here and there? What if I need to move std::vector to MyVector? May I just use swap() or not?

Do not produce new entities just to make something to look better. These entities (especially, such common) aren't going to live in vacuum. They will live in mixed environment with constantly increased entropy.

Custom allocator method is not called

The inheritance of the standard allocator is not needed. Remove the inheritance : public std::allocator<T> and the compiler will be kind to inform you about what you missed to implement. Until C++17 the method construct must be implemented and it is used in std::vector, not allocate. Also value_type, deallocator and destroy are missing.

template< class U, class... Args >
void construct( U* p, Args&&... args );

Since you haven't implemented it and your allocator inherits the standard allocator, std::allocator::construct is called, that does not produce output.

Why you should not inherit std::allocator

The answer on this question is simple on the one hand and not simple in practice on the other hand.

  1. Like other classes in C++ standard library, std::allocator does not have a virtual destructor, so it should not be inherited, if it is not explicitly indicated like in std::enable_shared_from_this.
  2. Standard containers don't use class Allocator directly. They use std::allocator_traits. It helps to implement minimal user-defined allocators. If you implement only value_type, allocate and deallocate members of MyAllocator, std::allocator_traits<MyAllocator> makes MyAllocator fully conformed to C++ named requirements: Allocator.

    • Let look at your MyAllocator carefully. It inherits std::allocator and "replaces" std::allocator::allocate (2).
    • Let read std::allocator_traits::allocate (2) reference manual, that is called by std::vector:

      Calls a.allocate(n, hint) if possible. If not possible (e.g. a has no two-argument member function allocate()), calls a.allocate(n).

    What you have reached. You have implemented MyAllocator::allocate(std::size_t n) (2), but not MyAllocator::allocate(std::size_t n, const void* hint) (1), it is inherited from std::allocator. It is called from std::vector, that is not what you expected. If you had not inherited std::allocator, your implementation MyAllocator::allocate would be called.

What's the advantage of using std::allocator instead of new in C++?

std::allocator is the default memory allocator for the standard library containers, and you can substitute your own allocators. This allows you to control how the standard containers allocate memory. But I don't think that your question is about std::allocator specifically, but rather the strategy of allocating memory, then constucting objects in that memory, rather than using new T[N], for example.

And the reason for that is that new T[N] doesn't allow you control over what constructors are called. And it forces you to construct all your objects at the same time. This is terrible for the purposes of, for example, std::vector where you only want to allocate occasionally.

With a raw memory allocator, you can allocate a certain amount of memory, which determines your capacity. Then, as the user adds items to the vector (using the constructor of their choice), you can construct objects in place in this memory.

Then when you run out of memory, you allocate more, typically twice as much. If std::vector used new T[N], it would have to reallocate every time you wanted to add or remove an element, which would be terrible for performance. You would also be forced to use the default constructor for all the objects, which puts an unnecessary restriction on the types of objects std::vector can hold.

Why do C++ allocator requirements not require that construct() constructs an object of value_type?

Note that a.construct(xp, args) is only optional. A std::vector merely needs allocate() to allocate memory and then it can use placement-new to construct objects in that memory. If the allocator has construct() then the vector can use it to create objects (also of type T) in memory previously obtained via allocate().

a.allocate:

Allocates storage suitable for an array object of type T[n] and creates the array, but does not construct array elements. May throw exceptions.

And returns a pointer to allocated memory.



I think I don't understand. X can be any type including T. Isn't that mean X can be something that is not T, so vector cannot use construct if the allocator it uses have X which is not equal to T?

No. The list is "Given..." and has the bullet:

  • xp, a dereferenceable pointer to some cv-unqualified object type X

That is, you, the caller, picks some X. Then the allocator has to meet the requirement that a.construct(xp, args) constructs an object of type X. What X actually is, that is not specified, hence whatever xp with type X you choose, the allocator has to meet that (optional) requirement.


As an analogon, consider someone writes some multiplication method and the requirement is:

Given some x and y of type int:

  • the method returns the result of x*y.

Now, "some x and y of type int" is what you choose, while "the method returns the result of x*y" is the requirement the method has to fulfill. It's not the method that chooses some x and some y and returns 42 always, because there are values x==1 and y==42 where it yields correct results. Rather, it means that for any x and any y you choose, the method should fullfill "the method returns the result of x*y" (for sake of simplicity, I ignored overflow here).

Is it okay to inherit implementation from STL containers, rather than delegate?

The risk is deallocating through a pointer to the base class (delete, delete[], and potentially other deallocation methods). Since these classes (deque, map, string, etc.) don't have virtual dtors, it's impossible to clean them up properly with only a pointer to those classes:

struct BadExample : vector<int> {};
int main() {
vector<int>* p = new BadExample();
delete p; // this is Undefined Behavior
return 0;
}

That said, if you're willing to make sure you never accidentally do this, there's little major drawback to inheriting them—but in some cases that's a big if. Other drawbacks include clashing with implementation specifics and extensions (some of which may not use reserved identifiers) and dealing with bloated interfaces (string in particular). However, inheritance is intended in some cases, as container adapters like stack have a protected member c (the underlying container they adapt), and it's almost only accessible from a derived class instance.

Instead of either inheritance or composition, consider writing free functions which take either an iterator pair or a container reference, and operate on that. Practically all of <algorithm> is an example of this; and make_heap, pop_heap, and push_heap, in particular, are an example of using free functions instead of a domain-specific container.

So, use the container classes for your data types, and still call the free functions for your domain-specific logic. But you can still achieve some modularity using a typedef, which allows you to both simplify declaring them and provides a single point if part of them needs to change:

typedef std::deque<int, MyAllocator> Example;
// ...
Example c (42);
example_algorithm(c);
example_algorithm2(c.begin() + 5, c.end() - 5);
Example::iterator i; // nested types are especially easier

Notice the value_type and allocator can change without affecting later code using the typedef, and even the container can change from a deque to a vector.



Related Topics



Leave a reply



Submit