Currently, allocators seem to be absolutely required to be copy constructable. This is actually a very stringent requirement; the copy constructed allocator must compare equal to the original allocator, which for allocators implies that they can allocate and deallocate each other's pointers. This is fine if your allocators simply hold pointers back to a memory pool. But what if your allocators want to own their own resources? Such allocators can never really allocate or deallocate each other's pointers, so they can never compare equal. This sounds like a big deal, but if your allocator is uncopyable, then it's not an issue; there's no real situation where you would ever necessarily expect them to compare equal. The current requirement of copyability is artificial though, all of the tools are already in place to avoid it. An allocator that sets propagate_on_container_copy_assignment to false already doesn't need to be copy assignable. And if an allocator defines select_on_container_copy_construction it is never "conceptually" copied. Consider the following example:
#include <vector>
#include <iostream>
template <class T>
struct SimpleAllocator {
typedef T value_type;
SimpleAllocator() = default;
template <class U> SimpleAllocator(const SimpleAllocator<U>& other) {};
T* allocate(std::size_t n) { return new T[n]; };
void deallocate(T* p, std::size_t n) { delete [] p; };
SimpleAllocator(const SimpleAllocator&) { std::cerr << "copy\n"; };
SimpleAllocator(SimpleAllocator&&) = default;
SimpleAllocator& operator=(const SimpleAllocator&) = delete;
SimpleAllocator& operator=(SimpleAllocator&&) = default;
SimpleAllocator select_on_container_copy_construction() const {
std::cerr << "blah\n";
return SimpleAllocator{};
}
};
template <class T, class U>
bool operator==(const SimpleAllocator<T>&, const SimpleAllocator<U>&) { return true; }
template <class T, class U>
bool operator!=(const SimpleAllocator<T>&, const SimpleAllocator<U>&) { return false; }
int main(int argc, char **argv) {
std::vector<double, SimpleAllocator<double>> x{1.0};
std::cerr << x[0] << "\n";
auto y = x;
}
This example runs and prints:
So constructing our container, which should simply default construct an allocator, also makes a copy, despite there being no "conceptual" copy. When we copy construct, we create a new allocator using select_on_container_copy_construction, and copy that new allocator, which is also unnecessary.
If these unnecessary copies were eliminated, then it would actually be quite simple to write allocators that owned their own resources. Such an allocator just needs a default constructor and move/swap, which is typically These have multiple applications:
- For data structures that perform many small allocations (like map and set), such an allocator would allow making some speed vs space trade-offs by chunking up allocations. This can be done in the current framework, but requires introducing a memory pool, which often has separate lifetime, or perhaps is co-owned by its allocators via shared_ptr. Using an owning allocator is much simpler and hassle free; it is a drop in replacement with a typedef.
- It allows one to force the small X optimization on any data structure (less efficiently, of course). For instance, one could create an allocator that deliberately contains 64 bytes of empty storage. Allocation requests for 64 bytes or less are served from this buffer, anything larger is sent to the heap. Convenient e.g. for std::function, or std::vector, and easy to customize.
- It has interesting possibilities combined with scoped_allocators. I admit I haven't investigated these in depth, but it seems like it would allow scenarios similar to the first example; e.g. consider a vector<string>. The inside strings are likely to make many small allocation requests. If the vector has a scoped allocator that owns it own resources (i.e. allocates large chunks of memory), then it can give the strings allocators that reference the outer allocator. Again, the same can be accomplished with memory pools, but this is cleaner.
My proposal would basically be to change the Allocator concept so that copy constructability is only necessary if select_on_container_copy_construction is not defined, or if the allocator uses it internally. In turn, AllocatorAware would be changed to never make unnecessary copies; which would also mean changing the standard library. This does not require any core language changes, and what's more it is backwards compatible. Since it broadens the definition of legal allocators, all existing allocators would continue to be allocators and usable in the STL. It would mean that technically, user written AllocatorAware containers would not be strictly compliant, but they would keep working with std::allocator (which is copyable of course) and any allocators they had written themselves.
Thoughts and criticism welcome!