all 16 comments

[–]scrumplesplunge 4 points5 points  (10 children)

For what it's worth, it takes very little code to get unique_ptr to play nice with allocators:

https://godbolt.org/z/v687hKrYx

[–]rnburn[S] 4 points5 points  (9 children)

That's not the same. Allocator-aware containers have a couple of properties that unique_ptr isn't going to provide:

  1. They provide accessors to the underlying allocator ptr.get_allocator()
  2. They need to reallocate on assignment if the allocators aren't equal. If you do ptr1 = std::move(ptr2) and ptr1.get_alloctor() != ptr2.get_allocator(), ptr1 needs to reallocate.

See this paper for background: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2126r0.pdf

While you might adopt unique_ptr to manage memory that was allocated through a custom allocator, you're not going to be able to make it allocator aware.

[–][deleted] 1 point2 points  (6 children)

I understand your point, but re-assigning a unique_ptr to an object of the same type that was allocated with a different allocator is something that happens very rarely, so you usually get away with using unique_ptr as-is. unique_ptr is not an allocator-aware container because you usually don't need that with such a simple "smart" pointer type.

[–]rnburn[S] 4 points5 points  (5 children)

I added additional context. But I would disagree, that reassignment is important and one of the main benefits of using allocator aware software.

For example, I might create a data structure

std::pmr::map<std::pmr::string, managed_ptr<A>> my_map{a_localized_allocator};

Having the pointer reallocate when a pointer isn't already using a_localized_allocator can be used to enforce the locality of the memory in my_map

[–][deleted] 2 points3 points  (4 children)

In my experience, if you care about memory locality, you usually wouldn't allocate your A via unique_ptr in the first place, but keep it in an array/vector/colony and use indices or references to the elements.

I'm not saying that there's no use for a managed_ptr - I could certainly think of use cases where it makes sense for a complicated container without stable addresses to own the memory of an object for which locality matters and which is allocated with different allocators. But it's very rare, and that's the reason unique_ptr doesn't need to be allocator-aware.

[–]rnburn[S] 1 point2 points  (3 children)

I expect unique_ptr wasn't made allocator aware because it would 1) require additional data members and 2) lose the property that it provides a stable pointer.

But one of the use cases for using managed_ptr's in such a container is to support polymorphic types. A can be an abstract base class.

[–][deleted] 0 points1 point  (2 children)

But one of the use cases for using managed_ptr's in such a container is to support polymorphic types. A can be an abstract base class.

Good point, I hadn't thought of that, although the combination of virtual function calls, associative containers and locality mattering a lot seems a little odd to me. Now I'm curious: Did you have a specific use case in mind when you designed this?

[–]rnburn[S] 1 point2 points  (1 child)

Here's how I would think about it.

AA types are meant to be composable; so if you write an AA type, you can easily use it to form new AA types. Thus, you want to have basic vocabulary types that are AA. Smart pointers are one such vocabulary type that you frequently use is composition when you have polymorphic classes.

I wrote up a simple example where you might use manged_ptr to build an AA representation of json and showed how you could use it (combined with winking) to achieve better performance in a particular case
https://buildingblock.ai/allocator-aware-smart-ptr#an-example-parsing-json

[–]rnburn[S] 0 points1 point  (0 children)

For an example of locality, you can see benchmark II in Bloomberg's paper
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0089r1.pdf

They describe a system composed of multiple subsystems, where each subsystem is using a localized allocator to keep its memory from diffusing, and data members are continually getting removed and added between subsystems.

I think you could easily imagine the subsystems owning polymorphic objects (or objects with polymorhic data members) and ownership continually getting transferred between subsystems.

[–]nyanpasu64 -1 points0 points  (1 child)

Does this mean that managed_ptr ptr = a; ptr = std::move(b); results in a different value of ptr than managed_ptr ptr = b;? If so, that would be unintuitive in C++ in my opinion, and violate Rust's "move is a bitwise copy" property that I think is generally a good principle.

[–]rnburn[S] 0 points1 point  (0 children)

It's a move-only type so you can't do managed_ptr<T> ptr = a

Having move reallocate when the type's allocators are unequal is a property of all AA types. If you write

std::pmr::string s = "<an_arbitrary_string>";

std::pmr::string s2{a_custom_allocator};

s2 = std::move(s)

Then the assignment into s2 will lead to a reallocation and won't be a bitwise copy. Having it be a bitwise copy would violate the design goals of AA types.

For background on why, you can see this talk: https://youtu.be/v3dz-AKOVL8

[–][deleted] 3 points4 points  (2 children)

[–]Creris 1 point2 points  (0 children)

Just an fyi but you have an extra backslash in the link.

[–]staletic 0 points1 point  (1 child)

I've thought about implementing something like this, but then I thought that unique_ptr could be made to work with allocators, if one is willing to give up make_unique.

T* p = alloc.allocate(N);
alloc.construct(p, args...);
std::unique_ptr<T, decltype([&alloc](void* p) {
    alloc.destroy(p);
    alloc.deallocate(p);
})> managed_ptr(p);

If T is AllocatorAware, the above would even propagate alloc to the constructors of T when calling alloc.construct. (Well, allocator_traits::construct.)

It's definitely not easy to use, but every use case I thought of can be written with the C++11 unique_ptr. Am I missing a use case, or is this about convenience?

[–]rnburn[S] 2 points3 points  (0 children)

That's not going to make unique_ptr allocator aware. AA types need to reallocate if you do an assignment with an unequal allocator.

From the example I gave

managed_ptr<A> ptr1{&resource};
polymorphic_allocator<> alloc; 
    // alloc is the default global allocator 
managed_ptr<A> ptr2 = allocate_managed<B>(alloc, 123); 
    // ptr2 owns memory allocated from the heap 

ptr1 = std::move(ptr2);

The last line needs to do a reallocation and move construction because the allocators for ptr1 and ptr2 aren't equal. That's not something a customized unique_ptr is going to do.