How to use std::scoped_allocator_adapter?
Asked Answered
F

1

10

As far as I understand, std::scoped_allocator_adapter provides a control mechanism for specifying separately which allocator will be used by a container, by its elements, by elements of their elements, etc. assuming the elements themselves are containers.

That said, I am having trouble understanding the semantics of std::scoped_allocator_adapter.

Bjarne Stroustrup provides the following 4 examples in The C++ Programming Language, section 34.4.4, pg. 1001(I will refer to them as Example-1, Example-2, etc. in my questions to follow.):

We have four alternatives for allocation of vectors of strings:

// vector and string use their own (the default) allocator:
using svec0 = vector<string>;
svec0 v0;

// vector (only) uses My_alloc and string uses its own allocator (the default):
using Svec1 = vector<string,My_alloc<string>>;
Svec1 v1 {My_alloc<string>{my_arena1}};

// vector and string use My_alloc (as above):
using Xstring = basic_string<char,char_traits<char>, My_alloc<char>>;
using Svec2 = vector<Xstring,scoped_allocator_adaptor<My_alloc<Xstring>>>;
Svec2 v2 {scoped_allocator_adaptor<My_alloc<Xstring>>{my_arena1}};

// vector uses its own alloctor (the default) and string uses My_alloc:
using Xstring2 = basic_string<char, char_traits<char>, My_alloc<char>>;
using Svec3 = vector<xstring2,scoped_allocator_adaptor<My_alloc<xstring>,My_alloc<char>>>;
Svec3 v3 {scoped_allocator_adaptor<My_alloc<xstring2>,My_alloc<char>>{my_arena1}};

For the sake of completeness, My_alloc is defined in the book like this:

template<typename T>
struct My_alloc { // use an Arena to allocate and deallocate bytes
    Arena& a;
    My_alloc(Arena& aa) : a(aa) { }
    My_alloc() {}
    // usual allocator stuff
};

Now, off to my questions:


Question-1: In Example-3, does the elements of Svec2 use My_alloc<char>{my_arena1} to allocate storage for their elements(of type char)?


Question-2: If so, does it have anything to do with the fact that Xstring uses the custom allocator My_alloc<char> in its definition? IOW, even if Xstring were defined with another allocator, wouldn't the elements of Svec_2 still use My_alloc<char>{my_arena1} since scoped_allocator_adaptor of Svec2_ dictates which allocator to use for allocations done by Svec_2 and allocations done by its elements(of type Xstring)?


Question-3: Consider the code below which is supposed to be a variation on Example-3. Does v2_ elements(of type std::string in this case) use My_alloc<char>{my_arena1}? If not, which allocator do they use?

// both v2_ and its elements to use My_alloc<some_type>{my_arena1}
using Svec2_ = 
    vector<string, scoped_allocator_adaptor<My_alloc<string>>> // value_type std::string, instead of Xstring 
Svec2_ v2_ {scoped_allocator_adaptor<My_alloc<string>>{my_arena1}};

Question 4: Based on the explanation on www.cppreference.com I quoted below, it seems to me that in Example-4, Svec3 does not use its default allocator std::allocator to allocate storage for its elements(of Xstring2 type), but rather the "outer allocator", which is My_alloc<xstring>. Elements of Svec3, on the other hand, will use MyAlloc<char> which is the "inner allocator". (And not because it is the allocator specified in the definition of Xstring2, but because it is the "inner allocator" in scoped_allocator_adapter of Svec3.)

This is what it says on www.cppreference.com in std::scoped_allocator_adaptor section:

Given:

template< class OuterAlloc, class... InnerAlloc > class
    scoped_allocator_adaptor : public OuterAlloc;

A container constructed directly with a scoped_allocator_adaptor uses OuterAlloc to allocate its elements, but if an element is itself a container, it uses the first inner allocator. The elements of that container, if they are themselves containers, use the second inner allocator, etc. If there are more levels to the container than there are inner allocators, the last inner allocator is reused for all further nested containers.

Based on that, shouldn't the correct example be as follows?

// v3_ uses its default allocator(or rather std::allocator), while its elements use My_alloc
using Xstring2 = basic_string<char, char_traits<char>, My_alloc<char>>;
using Svec3_ = 
    vector<Xstring2, scoped_allocator_adaptor<std::allocator<Xstring2>, My_alloc<char>>>;
Svec3_ v3_ {
    scoped_allocator_adaptor<std::allocator<Xstring2>, My_alloc<char>>{my_arena1}
};

This way, doesn't this use of scoped_allocator_adaptor dictate that allocations by Svec3_ use std::allocator, while its elements use My_alloc<char>{my_arena1} for their allocations?


I can tell I have fundamental misunderstandings of the scoped allocator model. Can someone point out what I am missing?

Furcula answered 21/2, 2017 at 10:27 Comment(5)
General point: I would always use a bunch of alias templates when using allocator adaptors to keep the notation clean, otherwise you just drown in noise. Start with something like template <typename T> using ScAlloc = std::scoped_allocator_adaptor<MyAlloc<T>>;. Then you can easily spell things like vector<T, ScAlloc<T>>.Correna
https://mcmap.net/q/416201/-what-is-the-purpose-of-std-scoped_allocator_adaptor/819272Saideman
@KerrekSB I was fortunate enough to notice the link you have provided in the brief time that it appeared in your comment. I have no idea why you decided to erase it, but it is indeed a great read. Nonetheless, the only example provided there(under propagation section) is very similar but not the same as the Example-3 in my post. (In Example-3, the container used as the element uses a non-scoped allocator, whereas in the mentioned link, it uses a scoped one.)Furcula
@Kemal: The linked guide didn't discuss multiple nested allocators in the scoped allocator adaptor, so I figured it was of no use for your problem. And yes, once you go down the scoped allocator route, you always want to use scoped allocators, which is why I would always start with the alias template.Correna
@Saideman I had already read that link before I posted mine. Yet again very informative post by Jonathan Wakely. Unfortunately it is not about the differences in usage that my post is rather about.Furcula
E
2

TL;DR

Question-1:
Yes.

Question-2:
No.

Question-3:
No.
Its default allocator (= std::allocator<char>()) is used.

Question-4:
You are right.
(But both Example-4 and its modified-version cannot be compiled because they lack a constructor argument for outer allocator.)

Can someone point out what I am missing?:
I don't think that you misunderstand something, but rather you only forget that My_alloc cannot be used for std::string because an allocator type of std::string is std::allocator, not My_alloc.

Remember that an allocator is just used for an constructor argument, so an allocator which have a different type cannot be used.
As Jonathan Wakely pointed out, when Svec2 or Svec2_ creates an its element, it calls:

std::allocator_traits<std::scoped_allocator_adaptor<My_alloc<xxx>>>::construct(
    get_allocator(), void_ptr, args...
);

where xxx is Xstring2 for Svec2 or std::string for Svec2_.
It calls get_allocator().construct(void_ptr, args...), and then it calls any one of:

std::allocator_traits<My_alloc<xxx>>::construct(
    outer_allocator(), void_ptr, std::allocator_tag, inner_allocator(), args...
);

if constructor call xxx(std::allocator_tag, inner_allocator(), args...) is well-formed.

or

std::allocator_traits<My_alloc<xxx>>::construct(
    outer_allocator(), void_ptr, args..., inner_allocator()
);

if constructor call xxx(args..., inner_allocator()) is well-formed.

or

std::allocator_traits<My_alloc<xxx>>::construct(
    outer_allocator(), void_ptr, args...
);

if constructor call xxx(args...) is well-formed.

In these examples, both outer_allocator() and inner_allocator() have the type My_alloc<xxx>.
The std::basic_string and all allocator-aware containers have constructors whose last argument is an allocator for all constructor overloads.
So, if the inner_allocator() (the type My_alloc<xxx>) is convertible to the allocator type of the elements, the second form is called (i.e. the allocator is used); otherwise the third form is called (i.e. the allocator is never used).

For Xstring2, the allocator type is My_alloc<char>, and My_alloc<Xstring2> is convertible to My_alloc<char>. So, the elements of Svec2 are constructed by Xstring2(args..., inner_allocator()), then their internal memory are allocated by My_alloc<char>.
By constrast, the allocator type of std::string is std::allocator<char>, and My_alloc<Xstring2> is not convertible to std::allocator<char>. So, the elements of Svec2_ are constructed by std::string(args...), then their internal memory are allocated by default allocator (= std::allocator<char>()).

FYI: Strictly speaking, it is determined by std::uses_allocator whether the third form is used or not.

Estriol answered 16/8, 2018 at 6:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.