Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save grantrostig/0bc0ade50aa96f63f6a1159b7e48119d to your computer and use it in GitHub Desktop.
Save grantrostig/0bc0ade50aa96f63f6a1159b7e48119d to your computer and use it in GitHub Desktop.
C++: Stateless vs. Statefull Allocators

C++: Stateless vs. Stateful Allocators

Categorizing Allocators

All standard C++ library conforming allocators have to support the minimal interface requirements. Beyond these requirements, some examples of fully defined allocators are LLVM's latest standard C++ library implementation of std::allocator, and a custom allocator I've written called ThreadLocalAllocator. These are both examples of what's been categorized as "stateless" allocators. Alternatively, allocators can be "stateful".

Looking at implementation examples like these, they're not just simply stateless of course. Rather, these categories refer to whether or not created allocator objects themselves are empty, likely because they use some global state instead, or whether they have non-static data members. Incidentally, Andrei Alexandrescu gave what to me is an entertaining & insightful talk on this and more related to allocators. A benefit of objects being empty, in this sense, is the empty base class optimization such that created objects of allocator using types, like std::vector, don't get any bigger for using any stateless allocators.

Note that neither of these terms appear in the C++17 standard, nor the C++20 standard. They do appear however in places like the C++ standards library working group (LWG) issue 2108: No way to identify allocator types that always compare equal.

Operator==

Another perspective on stateless versus stateful, regards operator==(const Alloc& a1, const Alloc& a2).

From this perspective, two allocators comparing equal suggests a1 can deallocate the memory of a2, and vice-versa. Thats more likely possible for allocators using the same underlying memory resources. For a stateless allocator, that's always the case - that they'd both share the same underlying memory resource. Meanwhile, for stateful allocators, that's usually only the case if they both have the same configured state in terms of the underlying memory resource they use.

Unfortunately, this doesn't address a possibility that deallocation might be sensitive to other things, like the thread of the allocating caller versus the thread of the deallocating caller being different. User beware.

C++17 Onwards

The C++17 standard introduces a standardized interface to a memory resource via the std::pmr::memory_resource class. This facilitates the use of the std::pmr::polymorphic_allocator class template also introduced in C++17. memory_resource can help with either category of allocators, but is particularly of use with polymorphic_allocator for "stateful" allocators. That's because polymorphic_allocator holds a pointer to a memory_resource object (in a non-static data member), can override its default memory_resource object via one of its constructors, and uses that object as the memory resource it allocates and deallocates from.

Examples

Here's an example of using a stateless allocator:

std::vector<int, std::allocator<int>> my_ints; // same as std::vector<int> my_ints!

And here's an example of using a stateful allocator, with something other than the default memory resource:

std::pmr::synchronized_pool_resource resource;
std::vector<int, std::pmr::polymorphic_allocator<int>> my_ints(&resource);

Note that my_ints takes a pointer to the resource object and the resource object has to stay in scope for the lifetime of my_ints.

Let's Get Weird

When doing copy assignment, for example int a = 5; int b; b = a;, convention suggests this results in equality. I.e. that then b == a. Or moreover, that a's state is then the same as b's. This property or expectation is sometimes also referred to as regularity.

Things get a little strange however when doing copy assignment, or move assignment for that matter, of types that use stateful allocators. One of the member functions common to these types, is get_allocator(). This returns a copy of the object's allocator.

So for example calling myInts.get_allocator() where myInts is defined as:

std::vector<int, std::pmr::polymorphic_allocator<int>> myInts(&resource);

Results in the value of std::pmr::polymorphic_allocator<int>(&resource).

Now suppose we define another vector of the same type but that uses a different memory resource on construction, and then assign myInts to it as in:

std::vector<int, std::pmr::polymorphic_allocator<int>> otherInts(&otherResource);
otherInts = myInts;

If we then call otherInts.get_allocator() what do you guess we get? Hint: we don't get myInts.get_allocator().

We get, std::pmr::polymorphic_allocator<int>(&otherResource).

In other words, while the vector element state data gets copied on assignment, the allocator used does not!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment