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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With