A brief introduction to the C++ allocator model (2023)

my roleP1144R8“std::is_trivially_relocatableThree (!) Bloomberg articles have been added in recent months:

  • P2786R1“trivial relocation options” (Jill, Meredith)
  • P2839R0"Non-trivial relocations using a new proprietary reference type" (Bi, Bern)
  • P2814R0"Comparing P1144 and P2786" (Gill, Meredith, O'Dwyer)

The reason for this sudden interest is that trivial relocation results in a surprising interplay with one of Bloomberg's favorite interests: the polymorphic allocator model, known as the "PMR"Umadmission tomorrowI'll show you some of these amazing interactions, but today we'll lay the groundwork by briefly describing the current factory model and its points of customization.

In this article, I will refer to the "std::pmr" or "PMR", but note that the Bloomberg standard library implementation itself ("BSL") Do not usestd::pmr! Instead, it defines the BSLits opponentstd::allocatorBe a country allocator, not a promoterstd::pmr::polymorphic_allocatorfor youmode::vectorBy default it is similar to PMR. You'd have to look really hard on Bloomberg for a container that works the same way as plain C++ STL.

The basic idea is that instead of mining memory allocations from the large global heap, PMR allows us to create smaller local memory regions. When we create an object (in the classic OOP sense), we decide which memory region it will be bound to. Any allocations related to this object will be from the same memory region. If the object contains avector, the dynamically allocated array buffer will come from the object field. in the case ofvector, then each string in the array will be allocated from the object area. etc.

The standard library uses the term"memory resources"instead of "arena", but I won't do that in this post to avoid confusion with "resource management".zarenas ("memory resources"), but they are not arenas ("memory resources") themselves.

In the polymorphic allocator model, the "transfer of ownership" of memory allocation is only possible for objects in the same arena. if objectO1live in the arenaA1and objectoxygenlive in the arenaA2followed by "tasks", e.g.O1 = mode::move(O2);Can't move any pointeroxygenthemO1- Pointing to the wrong arena! Assignment must allocate a new copyoxygengiven on stageA1.This completely weakens the motion semantics in C++11...but that's fine, because motion semantics are a subset of value semantics, and that's what we're doing herenovalue semantics. In the world of PMR, we are not interested in platonic objectsvaluesbut with themidentity,Just like in classic OOP.

I believe in this caseO1 = O2;is solecism: in an ideal world it would be written in the classic sitting formO1-in-memory-and-poke-at-to" sintaxe,O1.populateFrom(O2),riceoxygenIt will advertise itself as immobile instead of pretending to be moving, but it isn't. But the STL will require two differentvector- It's like using two completely different APIs instead of being able to reuse the same template for both.mode::vectorDoesn't change its API based on whether or notAThis isstd::allocatoror a PMR-style state allocator; it just changes the APIsignificance.

For more information, seeThis Bloomberg GitHub wiki page does not have a BDE allocator model.(However, I've been told it's outdated.)

pure C++C++17PMRBSL
(no equivalent)std::pmr::memory resourcebslma::Allocator
(global stack)std::pmr::get_default_resource()bslma::default::alokator(nullptr)
(no equivalent)std::pmr::monotonic_buffer_resourcebdlma::BufferedSequentialAllocator
(no need)std::uses_allocator_vbslma::Uses the Bslma allocator
std::vector<:string>v;v.push_back("For example, hello world");
char buf[10'000];auto mr = std::pmr::monotonic_buffer_resource(buf, sizeof(buf));auto v = std::pmr::wektor<:pmr::string>(& mr); v.push_back("Hello world for example");assert(v[0].get_allocator().resource() == &mr);
char buf[10'000];auto mr = bdlma::BufferedSequentialAllocator(buf, sizeof(buf));auto v = bsl::wektor<:string>(&mr);v.push_back("Hello world, 例如");assert(v[0].get_allocator().mechanism() == &mr);

Before we continue, you should also be aware of and internalize the subtle inefficiencies of the naive PMR and BSL snippets above.wetor::push_backHere an rvalue reference to a is requiredpmr::stringThe temporary we build with the transform buildercharacter constant*and the default memory area (global heap). Then,rewindPut a copy of this temporary file into the vector usingOrderExtension allocators carry constructors. This will copy characters from the default memory area towmemory arena itselfpan). Eventually, the original scratch space is destroyed, freeing the arena's default allocation.

For efficiency, we can writev.emplace_back("Hello..."), which removes the temporary, albeit at a costfluffy model. or,v.push_back(std::pmr::string("olá...", &mr)), who temporarily use the samew.

"Plain C++" code segments don't suffer from this inefficiency, because temporary strings and arrays share the same memory area: the global heap.


remember the difference betweenpushback(x)riceemplace_back(argument...): first time applicationX' move or copy constructor to insert a new element, the latter will use itX(parameters...).you can imaginestep backMake a new batch:

void *where = &data_[size_];::new (gdzie) value_type(std::forward(parameters)...); size_ += 1;

It actually adds an intermediate layer:

Use AT = std::allocator_traits;AT::construct(get_allocator(), gdzie std::forward(parameter)...);

allocator_traits::construct(a, gdzie, argumenty...)calla.construct(onde, argumenty...)If it's possible; otherwise it's just a new destination. For most allocators - includingstd::allocator- will be placed as new. But allocator authors can make their ownput upway of doing what they want.

Bez STL, toppmr::polymorphic_allocatorriceScoped_allocator_adaptorprovide extraordinaryput upmethod.

Allocator extension constructor

remember itpmr::polymorphic_allocatorwants to impose an invariant that each "level" of avector>>Use the same arena. It does this by intercepting every construction of the object - viaput up— and add itself as an additional parameter to the constructor. ifstd::allocatorBasically it does this:

templatestatic voidstruct(U *where, Args&&... args) { ::new (where) U(std::forward(parameter)...);}

Thenstd::pmr::polymorphic_allocatorBasically it does this:

templatevoidconstruct(U *where, Args&&... args) const { if constexpr (uses_allocator_v) { ::new (where) U(std ::forward(argument)..., *to); } else { ::new (onde) U(std::forward(parameters)...); }}

Here I am omitting some secret detailsConstruct using allocator. in real life,polymorphic allocator::constructorU.S.std::make_obj_using_allocatorWho is responsible for these arcane details? But if I showed you the real code below, you'd probably say "Hey! That doesn't mean anything!"

templatevoidconstruct(U *where, Args&&... args) const { ::new (where) U(std::make_obj_using_allocator(*to, std::forward(args)...));}

Every allocator-aware class type in C++ provides two versions of each of its constructors: a user-facing generic constructor and a user-consumable "allocator extension" constructor.std::make_obj_using_allocator. There are always two versions of every constructor, even special ones like standard constructors.

struct W { use allocator_type = A; explicit W(); // explicit default coefficient W(allocator_type); // default coefficient for allocator extension W(int); // explicit conversion from int constructor W(int , allocator_type); // extended version of allocator W(W&&); // move explicit actor W(W&&, allocator_type); // extended allocator action actor };

The first version is used by public code:

W w1;W w2 = 42;W w3 = std::move(w2);

The second version is usedstd::make_obj_using_allocator; its purpose is to createCobject and associate it with a specific memory region.

std::vectorv.emplace_back(); // construct W(v.get_allocator())

It takes a lot of machinery to get a simple result: the allocatorAcan (if you wish) providev[0]It's always built with the same allocator - the same memory region - aswi would like

Please notestd::allocatorDon't worry; there is no downward propagation of state. andstd::scoped_allocator_adaptorhas a different goal: it wants to associate each level with aDifferent kindsdispenser. Other (third-party) allocators may even have different purposes.

side spread

Finally, C++ allocators can propagate not only downwards, but also laterally—from one container object to another—as if the allocator were part of the container's value. This applies to all semantic value operations that take two arguments: copy assignment, move assignment, and replacement. For historical reasons, each of these three operations is performed byyour personality traits; but they are actually bundled together.

If your allocator propagates through container copy assignment, move assignment, and swap, it enables traditional move semantics: you can always steal the right container pointer, because you're stealing the allocator. On the other hand, this means that you don't care about the state of the original allocator - one allocator is as good as another - which means your allocator is effectively stateless, becausestd::allocator.

Since PMR allocators carry an important state (arena identity), they are not interchangeable and thus will not be propagated. They are "sticky". Remember that PMR is for classic OOP objects: an OOP object stays in one place for its lifetime, and its allocator stays there with it. distributed instd::pmr::vectorOrder=It doesn't change the arena.

you may ask what happens if i say twopmr::vectorHave different arenas? Well, technically, this is undefined behavior. Virtually every library sellerchange promptBut keeping the arena unchanged, this will cause heap corruption when the first vector goes out of scope and requires your arena to free a pointer that never came from that arena.generally considered a bad move.

Allocator is not important state

"a specification language for describing the semantics of copying values"(Lakos, 2007) Absolutelyout of stateas a material inside an object that contributes to itstrength.OstrengthThe abstract thing is then copied via copy operations, compared via comparison operators (if any), and so on. For examplesizeVector is a basic feature; butcapacityVectors don't matter.

is sand Apmr::vectorThe most important condition? No, because it is not copied by a copy operation. (if you copy and buildpmr::vector,you canStandard arena. If you copy and assign apmr::vector, osizecopy along with the reststrength, but the allocator remains the same. )

This is actually pretty obvious in current C++pmr::vectorNot a semantic value type, unless you add some preconditions on its irrelevant state. For example, you can list two semantic value objects of the same type; but by specifying two arbitratorspmr::vectoryour b. we can saypmr::vectorAs long as all related objects come from the samepmr::get_default_resource().

This caused endless pain for PMR when we were developing the semantic part of C++. untilN2479Predates C++11 flow semantics and is partially deprecated.

For example,P1825behavior changethis episodeBetween C++17 and C++20:

auto mr1 = std::pmr::monotonic_buffer_resource();auto &mr2 = *std::pmr::get_default_resource();std::pmr::string a[2] = { std::pmr::string("olá ", &mr1), std::pmr::string("mundo", &mr1),};std::wektor<:pmr::string>v;std::transform(std::make_move_iterator(a), std::make_move_iterator(a + 2), std::back_inserter(v), [](auto&& r) { return r; });#if __cplusplus > = 202000assert(v[0].get_allocator().resource() == &mr1);#elseassert(v[0].get_allocator().resource() == &mr2);#endif

No one (not even me!) noticed the change when it happened. I don't think we'd change course even if we noticed.

In the next post, we'll look at a few ways in which PMR null semantics can hurt us when trying to apply semantic value operations -- such as relocations! — for PMR objects.

  • "P1144 PMR Konecranes"(2023-06-03)


What is an allocator in C++? ›

Allocators are used by the C++ Standard Library to handle the allocation and deallocation of elements stored in containers. All C++ Standard Library containers except std::array have a template parameter of type allocator<Type> , where Type represents the type of the container element.

Why use allocators C++? ›

Some of the most common reasons for writing custom allocators include improving performance of allocations by using memory pools, and encapsulating access to different types of memory, like shared memory or garbage-collected memory.

Where are allocators used for? ›

Explanation: Allocators handle all the request for allocation and deallocation of memory for the container. 2. Where are allocators implemented? Explanation: Allocators are implemented in C++ standard library but it is used for C++ template library.

What is the meaning of the word allocator? ›

Definitions of allocator. a person with authority to allot or deal out or apportion. synonyms: distributor.

How C++ allocates memory? ›

C uses the malloc() and calloc() function to allocate memory dynamically at run time and uses a free() function to free dynamically allocated memory. C++ supports these functions and also has two operators new and delete, that perform the task of allocating and freeing the memory in a better and easier way.

What are the different types of allocation C++? ›

In C++ data can be allocated statically, dynamically on the stack, or dynamically on the heap. There are three categories of static data: global data, global class data, and static data local to a function. In C malloc , realloc and free are used to allocate memory dynamically on the heap.

Why use allocator instead of new? ›

new and delete are the direct way to create an object in dynamic memory and initialize it. Allocators are much more though, because they offer complete control over the aforementioned phases. With allocator we must explicitly allocate heap memory, construct it, destroy it, and then finally deallocate the memory.

Do you need to allocate memory in C++? ›

You're right that in C++ you rarely need to allocate memory manually. There are instances where that's the easiest way though1. The point is that C++ makes the manual deallocation completely unnecessary because destructors will take care of that.

Which function is used for memory allocation in C++? ›

The operators new and delete are utilized for dynamic memory allocation in C++ language, new operator is used to allocate a memory block, and delete operator is used to de-allocate a memory block which is allocated by using new operator.

What is an example of allocated? ›

Example Sentences

Money from the sale of the house was allocated to each of the children. We need to determine the best way to allocate our resources. Have enough funds been allocated to finance the project?

What is an example of allocation? ›

One of the most common allocation examples in the modern world is supply and demand. Supply and demand describe the price associated with a resource that is based on the availability of a resource and how much consumers are willing to pay. This is common in free markets.

What is the meaning of allocation with example? ›

An allocation is an amount of something, especially money, that is given to a particular person or used for a particular purpose. There will be a closer review of funding allocations for future conferences. [ + for] During rationing we had a sugar allocation.

What is alloc and malloc in C++? ›

The alloc function is used to allocate a region or block of size bytes in length of the heap . The malloc function is used to allocate heap storage. Its name stands for memory allocation.

Where are C++ objects allocated? ›

Unlike Java, instances of classes ("objects") can be allocated on the stack in C++. That is, you do not need to use new to allocate an instance of an object.

What is the difference between allocation and initialization in C++? ›

An allocation means to provide space on the stack (or heap, if its malloc ed). Initialization means to assign a value to the variable. And Declaration tells the compiler that the variable exists. It could also mean the variable exists in some other object file if it is preceded by extern .

What does dynamically allocated mean C++? ›

Dynamic memory allocation in C/C++ refers to performing memory allocation manually by programmer. Dynamically allocated memory is allocated on Heap and non-static and local variables get memory allocated on Stack (Refer Memory Layout C Programs for details).


Top Articles
Latest Posts
Article information

Author: Nathanial Hackett

Last Updated: 29/09/2023

Views: 5729

Rating: 4.1 / 5 (52 voted)

Reviews: 91% of readers found this page helpful

Author information

Name: Nathanial Hackett

Birthday: 1997-10-09

Address: Apt. 935 264 Abshire Canyon, South Nerissachester, NM 01800

Phone: +9752624861224

Job: Forward Technology Assistant

Hobby: Listening to music, Shopping, Vacation, Baton twirling, Flower arranging, Blacksmithing, Do it yourself

Introduction: My name is Nathanial Hackett, I am a lovely, curious, smiling, lively, thoughtful, courageous, lively person who loves writing and wants to share my knowledge and understanding with you.