STL algorithms for trivial relocation
This is the second in a series of at least three weekly blog posts. Each post
(I,
II,
III,
IV)
will explain
one of the problems facing P1144 “std::is_trivially_relocatable
”
and P2786R0 “Trivial relocatability options” as
we try to (1) resolve their technical differences and (2) convince the rest of the C++ Committee
that these resolutions are actually okay to ship.
Today’s topic is new library algorithms for relocation.
What is relocation used for, in practice?
Relocation (trivial or otherwise) tends to be used in the following ways:
-
On single objects that are (at least conceptually) member subobjects; e.g. the move-constructors of SBO-optimized
any
andfunction
-
On non-overlapping arrays of objects; e.g.
Vector
reallocation and the move-constructor ofstatic_vector
-
On an array that overlaps itself; e.g.
vector::insert
(which needs to shift elements rightward before inserting the new element) andvector::erase
(which needs to shift elements leftward after destroying the old element)
You might say, “Wait, I don’t think that’s how
vector::erase
works!” For ordinary (non-trivial) types, you’re right: erasure works by move-assigning elements to the left, and then destroying the last (moved-from) element. But for trivially copyable types, this is tantamount tomemmove
; it is also tantamount tomemmove
for trivially relocatable types. Facebook’sfolly::fbvector
already works that way for types that arefolly::IsRelocatable
, and Bloomberg’sbsl::vector
does too for types that arebslmf::IsBitwiseMoveable
.
Finally, a use-case that remains hypothetical as far as I know:
- On a single object, producing a return value. This is the idea I explored two days ago in
“Making
priority_queue
andflat_set
work with move-only types” (2023-03-01); it’s used insidevector::displace_back
, although it doesn’t need to be.
Now, the “implementor” (the STL vendor) doesn’t need any help to implement these use-cases.
The implementor can use their intimacy with the compiler to do things that are, on paper,
undefined behavior; or they can rely on non-portable compiler intrinsics.
But when you, the ordinary programmer of something like BSL or Folly or Qt, want to implement
your own Vector
, Function
, or StaticVector
type, it’ll help if the STL provides
portable APIs that you can use in each of these cases.
Can’t I just use memmove
?
In practice, yes, that’s the state of the art in BSL, Folly, and Qt. But on paper, no, it’s undefined behavior for at least two reasons.
-
If I have two objects, it’s defined behavior to
memmove
between them (and then inspect the value of the destination object) only if the object’s type is trivially copyable ([basic.types.general]/3). -
If I have only one object, and
memmove
from it into some storage where there is no object yet, and then treat that storage as if it now held an object, that’s defined behavior only if the object’s type is implicit-lifetime ([intro.object]/10). (The storage must also have been properly acquired, but that part is hard to screw up in practice. See [intro.object]/13.)
Both P1144
and P2786
agree that it’s important for the STL to provide a public API that relocates
objects (trivially or otherwise) in a well-defined way. Neither paper is trying to
“bless” the current use of raw memmove
as seen in BSL, Folly, and Qt; we’re trying
to provide a blessed alternative.
Overview of existing algorithms
Three major libraries provide “prior art” for P1144: BSL, Folly, and Qt. We can imagine what the STL API should look like, by looking at what these libraries define for their own internal purposes.
Folly
Folly provides no generic algorithms, as far as I can tell. fbvector
defines
a member function M_relocate
which calls relocate_move(T* d_first, T* first, T* last)
followed by relocate_done(T*, T* first, T* last)
. relocate_move
calls either
D_uninitialized_move_a
or memcpy
;
relocate_done
calls either D_destroy_range_a
or nothing.
That same division of labor is seen in libc++’s vector
,
by the way.
Qt
Qt’s approach to relocation semantics is the cleanest. It provides these two algorithms:
template<class T, class N>
void q_uninitialized_relocate_n(T* first, N n, T* d_first);
template<class T, class N>
void q_relocate_overlap_n(T* first, N n, T* d_first)
q_uninitialized_relocate_n
relocates a non-overlapping array. This is the building block for Vector::reserve
and StaticVector
.
Qt’s version of static_vector
is called QVarLengthArray
, and
its move constructor
relocates using q_uninitialized_relocate_n
.
q_relocate_overlap_n
relocates an overlapping array left or right, handling overlap “by magic” just like memmove
does.
This could be the building block for Vector::insert
and Vector::erase
.
However, as of this post, Qt’s QList::erase
uses QMovableArrayOps::erase
which doesn’t use q_relocate_overlap_n
; it uses raw memmove
instead.
The naming roughly reflects the analogies that Qt’s authors see with the STL:
q_uninitialized_relocate_n
, likestd::uninitialized_copy
, writes into uninitialized memory, and likestd::copy_n
does not document whether the data is copied front-to-back or back-to-front.q_relocate_overlap_n
writes into a destination buffer that might initially hold objects, so its name doesn’t involve the worduninitialized
.
Qt doesn’t provide a single-argument relocation function. Its equivalent of std::any
,
named QVariant
, is SBO-optimized but simply refuses to store anything non-trivially relocatable
in its SBO buffer. So QVariant
’s move constructor
can simply copy the bits.
BSL
BSL has the largest zoo of helper algorithms by far: “bslalg_arrayprimitives.h” is 4800 lines long. But BSL has only one primitive relocation algorithm.
template<class T, class Alloc>
void ArrayPrimitives_Imp::destructiveMove(
T* d_first, T* first, T* last, Alloc a);
destructiveMove
relocates a non-overlapping array, just like q_uninitialized_relocate_n
;
this is the building block for Vector::reserve
and StaticVector
.
In fact, BSL’s documentation shows an example
of using this algorithm in vector::reserve
.
BSL has some other helpers, but (as in Folly) they divide the labor in ways unclearly related to relocation.
For example, bsl::vector::erase
calls
ArrayPrimitives::erase
,
which calls
ArrayPrimitives_Imp::erase
,
which uses memmove
to do the trivial relocation. (It can’t use BSL’s own destructiveMove
,
because destructiveMove
works only on non-overlapping arrays.)
P2786R0
Bloomberg’s P2786R0 “Trivial relocatability options” (Gill & Meredith, February 2023) proposes the following library algorithms:
template<class T>
requires (is_trivially_relocatable_v<T> && !is_const_v<T>)
void trivially_relocate(T* first, T* last, T* d_first) noexcept;
template<class T>
requires ((is_trivially_relocatable_v<T> && !is_const_v<T>) ||
is_nothrow_move_constructible_v<T>)
T* relocate(T* first, T* last, T* d_first) noexcept;
template<class InputIterator, class NoThrowForwardIterator>
requires is_nothrow_move_constructible_v<iter_value_t<NoThrowForwardIterator>>
NoThrowForwardIterator
move_and_destroy(InputIterator first, InputIterator last,
NoThrowForwardIterator d_first);
trivially_relocate
is specified to always trivially relocate.
It handles overlapping arrays “by magic,” just like memmove
and q_relocate_overlap_n
.
So it could be the building block for both Vector::insert
and Vector::erase
… except
that it cannot handle arbitrary types! It SFINAEs away when trivial relocation isn’t possible.
relocate
also handles overlapping arrays “by magic,” like memmove
.
It can almost handle arbitrary types; it relocates trivially if possible, falling back
to move+destroy. But it cannot handle types with throwing move constructors, such as
MSVC’s std::list
, unless they happen to be trivially relocatable.
(Instead of cleaning up after an exception, the way
std::uninitialized_move
does, relocate
is specified to SFINAE away when relocation might throw an exception.
Thus it never throws. P2786R0 missed marking it noexcept
, but I’ve fixed that above.)
Finally, move_and_destroy
relocates by relatively underspecified means. P2786R0 says
“We do not support overlapping ranges in this function,” which I think means it’s UB to
pass overlapping ranges to this function at all — as in BSL’s destructiveMove
,
Qt’s q_uninitialized_relocate_n
, and std::copy_n
. Alternatively, it might mean that
the order of traversal is fixed, making move_and_destroy
a suitable building block
for Vector::erase
but not Vector::insert
. std::copy
and
P1144R6’s uninitialized_relocate
follow that approach.
Since all of these algorithms are constrained, none of them can accurately be described
as a “building block” for any of our use-cases. For example, StaticVector
’s move-constructor
can’t be implemented in terms of move_and_destroy
because move_and_destroy
fails to compile
at all for C++03-era types such as
struct Widget {
std::string s_;
Widget(const Widget& w) : s_(w.s_) {}
void operator=(const Widget& w) { s_ = w.s_; }
};
and of course StaticVector
needs to be able to work with such types.
P1144R6
My P1144R6 “Object relocation…” (O’Dwyer, June 2022) proposed the following library algorithms:
template<class T>
T* relocate_at(T* source, T* dest);
template<class T>
T relocate(T* source);
template<class InputIterator, class NoThrowForwardIterator>
NoThrowForwardIterator
uninitialized_relocate(InputIterator first, InputIterator last,
NoThrowForwardIterator d_first);
template<class InputIterator, class Size, class NoThrowForwardIterator>
pair<InputIterator, NoThrowForwardIterator>
uninitialized_relocate_n(InputIterator first, Size n, NoThrowForwardIterator result);
relocate_at
relocates an object (by any means possible) from source
to dest
.
This is the building block for Function
and Any
.
relocate
relocates an object from source
into the return slot.
See “std::relocate
’s implementation is cute” (2022-05-18).
This is the building block for our hypothetical
Vector::displace_back
.
uninitialized_relocate
relocates by any means possible, and invariably from left to right,
like std::copy
. This makes it a suitable building block for Vector::reallocate
and
Vector::erase
, but not Vector::insert
.
uninitialized_relocate_n
is exactly the same: relocate by any means possible, from left
to right, like std::copy
(and not like std::copy_n
).
If you want to relocate from right to left, as in Vector::insert
, then (under P1144R6)
you must write something like this:
template<class BidirIterator, class NoThrowBidirIterator>
NoThrowBidirIterator
uninitialized_relocate_backward(BidirIterator first, BidirIterator last,
NoThrowBidirIterator d_last)
{
std::uninitialized_relocate(
std::make_reverse_iterator(last),
std::make_reverse_iterator(first),
std::make_reverse_iterator(d_last));
}
As usual, Alexander Stepanov was here 30 years ago. Instead of calling the C++98 STL’s
std::copy_backward
, you could have written:std::copy( std::make_reverse_iterator(last), std::make_reverse_iterator(first), std::make_reverse_iterator(d_last));
But that requires your STL vendor’s
std::copy
to have special-casememmove
’ing codepaths not only forTriviallyCopyable*
but also forstd::reverse_iterator<TriviallyCopyable*>
. No vendor does that. Instead, they providestd::copy
andstd::copy_backward
, each with a single special-case codepath forTriviallyCopyable*
. That’s much easier to implement.
Therefore, P1144R7 will propose to add uninitialized_relocate_backward
, too.
Comparison table
In the table below, ✓ means “does what you’d expect”; UB means “undefined or unpredictable/wrong behavior”;
SFINAE means “the call fails to compile, in a SFINAE-friendly way”; and “N/A” means “not applicable/don’t care.”
q_uninitialized_relocate_n
supports types with non-noexcept
move constructors, but at least fails to say
(and possibly fails to consider) what happens at runtime were a move constructor actually to throw.
T.r. types | Non-t.r. types | Throwing-move types | Rightward motion (`insert`) | Leftward motion (`erase`) | Non-pointer iterators | ||
STL Classic (non-relocating) | std::copy | N/A | N/A | ✓ | UB | ✓ | ✓ |
std::copy_n | N/A | N/A | ✓ | UB | UB | ✓ | |
std::copy_backward | N/A | N/A | ✓ | ✓ | UB | ✓ | |
cstring | memcpy | ✓ | UB | ✓ | UB | UB | SFINAE |
memmove | ✓ | UB | ✓ | ✓ | ✓ | SFINAE | |
Qt | q_uninitialized_relocate_n | ✓ | ✓ | ✓? | UB | UB | SFINAE |
q_relocate_overlap_n | ✓ | ✓ | ✓ | ✓ | ✓ | SFINAE | |
BSL | destructiveMove | ✓ | ✓ | ✓ | UB | UB | SFINAE |
P2786R0 | trivially_relocate | ✓ | SFINAE | SFINAE | ✓ | ✓ | SFINAE |
relocate | ✓ | ✓ | SFINAE | ✓ | ✓ | SFINAE | |
move_and_destroy | ✓ | ✓ | SFINAE | UB | ? | ✓ | |
P1144R6 | uninitialized_relocate | ✓ | ✓ | ✓ | UB | ✓ | ✓ |
uninitialized_relocate_n | ✓ | ✓ | ✓ | UB | ✓ | ✓ | |
P1144R7 | uninitialized_relocate_backward | ✓ | ✓ | ✓ | ✓ | UB | ✓ |