Document #: | P3516R0 [Latest] [Status] |
Date: | 2025-01-11 |
Project: | Programming Language C++ |
Audience: |
LEWG |
Reply-to: |
Louis Dionne <ldionne.2@gmail.com> Giuseppe D’Angelo <giuseppe.dangelo@kdab.com> |
P2786 is currently making progress through EWG and LEWG. In Poland, LEWG expressed a strong desire for that language feature to come with a proper user-facing library interface. This paper proposes such an interface. That interface is heavily influenced by the uninitialized algorithms that were originally proposed in P1144 which, after implementation experience in libc++, seem to be almost exactly what we need.
This paper aims to provide the high-level algorithms that all programmers will want to use, while purposefully not touching on some of the more contentious design points of P2786/P1144. The high-level algorithms proposed in this paper are compatible with essentially any definition of “trivial relocation” we might introduce in the language in C++26 or later.
Today, many algorithms and containers use some flavor of assignment, either as an implementation detail or as part of their API promise. In cases where the use of assignment is merely an implementation detail, writing the code in terms of relocation can often result in cleaner code and improved performance (since move-construction is often faster than move-assignment).
Furthermore, in a future where the language itself provides a
definition of relocation (and in particular trivial relocation),
algorithms and containers written in terms of relocation today would
benefit from a massive speedup for trivially relocatable types without
requiring any change. Indeed, the algorithms introduced in this paper
boil down to memmove
(or
memcpy
if we can prove the absence
of overlap) in trivial cases, which are fairly common.
We propose adding algorithms std::uninitialized_relocate
,
std::uninitialized_relocate_n
,
and std::uninitialized_relocate_backward
.
We also add the equivalent
ranges::
functions and the parallel algorithm overloads. At a high level, these
algorithms relocate a range of objects from one memory location to
another, which is a very useful primitive when writing containers and
algorithms.
However, the library interface proposed here is intentionally independent of the details of the underlying language feature.
Design points:
std::uninitialized_copy
,
with the difference that the new proposed relocation algorithms support
overlapping input/output ranges like
std::move
and std::move_backward
.
That is necessary in order to support most use cases where one is
relocating a range a bit “to the right” or a bit “to the left” of its
current location.std::vector::insert
requires in order to provide the basic exception safety guarantee (see
the examples below).input_iterator
s (or
bidirectional_iterator
s for the
backward variant) for the input range. This iterator category is
sufficient to implement these algorithms properly. However, it does mean
that the range overlap precondition cannot be checked at runtime for
some iterator categories. In practice, we would expect an implementation
that wishes to check this precondition to do so whenever the input range
is at least random access, which should be the vast majority of use
cases.forward_iterator
s for the output
range, which is necessary to perform cleanup if an exception is
thrown.In these examples, we ignore allocator support for simplicity, and we
use iterator
instead of
const_iterator
as the parameter
types to lighten the code. The “real” code for implementing these
functions does not look meaningfully different.
vector::emplace
/
vector::insert
Assignment based
|
Relocation based
|
---|---|
|
|
A few things are worth noting:
[position, end())
range in a moved-from state, and the vector size may or may not have
been incremented by 1. Whether this is actually the case or not depends
on which operation exactly throws. Either way, the user has no direct
means to know which elements have been left in a moved-from state. The
relocation-based implementation instead ends up with the elements in
[position, end())
being erased from the vector. It’s unclear whether one behavior is
really superior over the other, but both are conforming.std::memcpy
(or whatever the language definition would be).is_replaceable_v<T>
(and otherwise fallback to move-assignment). In the absence of
is_replaceable
, a conservative
implementation could use std::is_trivially_copyable
as a guard.vector::erase
Assignment based
|
Relocation based
|
---|---|
|
|
Things worth noting:
vector::emplace
,
an implementation that considers the usage of move-assignment to be part
of the function’s API may want to preserve that behavior by further
constraining the usage of relocation.std::vector::erase
.
The current specification requires vector::erase
not
to throw an exception unless T
’s
move-assignment operator throws. The implementation above can
also throw if the move-constructor throws. We view this as an
overspecification in the Standard that could be relaxed – this is
probably an artifact of the fact that implementations were assumed to
use assignment. To make the definition above pedantically correct, we
should only use relocation when is_nothrow_move_constructible<T>
is satisfied, and otherwise use assignment.vector::emplace_back
/ vector::push_back
Note that the vector growth policy presented here is more naive than
what
std::vector
typically does. Also keep in mind that we must provide the strong
exception safety guarantee here.
Move-construction based
|
Relocation based
|
---|---|
|
|
Things worth noting:
args...
is
in fact a single object located inside the vector we’re inserting in.
Supporting that essentially requires creating a temporary value or
constructing the new element before we start moving from the current
vector, but does not meaningfully change the code.If we had trivial relocation in the language, these algorithms would not need to change significantly, assuming that trivial relocation is equivalent to move + destroy when both are valid. We can’t imagine a trivial relocation design where that wouldn’t be the case.
If C++ gets a notion of trivial relocation that increases the expressive power over move + destroy (like P2786), then these uninitialized algorithms should be slightly generalized to support types that are not movable but that are still trivially relocatable. Thus, these algorithms would be able to operate either on types that are movable, or on types that are not movable but trivially relocatable instead. That would be a small backwards compatible change.
Modify 26.2 [algorithms.requirements] as shown:
InputIterator
,
InputIterator1
, InputIterator2
, or
NoThrowInputIterator
,
the template argument shall meet the Cpp17InputIterator
requirements ([input.iterators]).OutputIterator
,
OutputIterator1
, or
OutputIterator2
, the template
argument shall meet the Cpp17OutputIterator requirements
([output.iterators]).ForwardIterator
,
ForwardIterator1
,
ForwardIterator2
, NoThrowForwardIterator
, NoThrowForwardIterator1
,
or
NoThrowForwardIterator2
,
the template argument shall meet the Cpp17ForwardIterator
requirements ([forward.iterators]) if it is required to be a mutable
iterator, or model forward_iterator
([iterator.concept.forward]) otherwise.NoThrowForwardIterator
,
the template argument is also required to have the property that no
exceptions are thrown from increment, assignment, or comparison of, or
indirection through, valid iterators.BidirectionalIterator
,
BidirectionalIterator1
, BidirectionalIterator2
, NoThrowBidirectionalIterator1
,
or
NoThrowBidirectionalIterator2
,
the template argument shall meet the Cpp17BidirectionalIterator
requirements ([bidirectional.iterators]) if it is required to be a
mutable iterator, or model
bidirectional_iterator
([iterator.concept.bidir]) otherwise.RandomAccessIterator
,
RandomAccessIterator1
, or
RandomAccessIterator2
, the template
argument shall meet the Cpp17RandomAccessIterator requirements
([random.access.iterators]) if it is required to be a mutable iterator,
or model random_access_iterator
([iterator.concept.random.access]) otherwise.NoThrowInputIterator
,
NoThrowForwardIterator
,
NoThrowForwardIterator1
,
NoThrowForwardIterator2
,
NoThrowBidirectionalIterator1
,
or
NoThrowBidirectionalIterator2
,
the template argument is also required to have the property that no
exceptions are thrown from increment, assignment, or comparison of, or
indirection through, valid iterators.Modify 26.11.1 [specialized.algorithms.general] as shown:
voidify
template<class T>
constexpr void* voidify(T& obj) noexcept {
return addressof(obj);
}
+template<class Source, class Dest>
+ concept relocatable-from =
+ same_as<Source, Dest> &&
+ move_constructible<Dest>;
+
+template<class T>
+ requires relocatable-from<T, T>
+ constexpr T* relocate-at(T* dest, T* source) {
+ struct guard { T *t; ~guard() { destroy_at(t); } } g(source);
+ return ::new (voidify(*dest)) T(std::move(*source));
+ }
Add at the end of 26.11.2 [special.mem.concepts]:
template<class I>
concept nothrow-bidirectional-iterator = // exposition only
<I> &&
nothrow-forward-iterator<I> &&
bidirectional_iterator<I, I>; nothrow-sentinel-for
bidirectional_iterator
([iterator.concept.bidir]) operations to throw exceptions. — end
note]template<class R>
concept nothrow-bidirectional-range = // exposition only
<R> &&
nothrow-forward-range<iterator_t<R>>; nothrow-bidirectional-iterator
In 20.2.2
[memory.syn], add to
the <memory>
header synopsis (before [unique.ptr]
):
template<class NoThrowInputIterator, class NoThrowForwardIterator>
constexpr NoThrowForwardIterator
(NoThrowInputIterator first, // freestanding
uninitialized_relocate
NoThrowInputIterator last,);
NoThrowForwardIterator result
template<class ExecutionPolicy, class NoThrowForwardIterator1, class NoThrowForwardIterator2>
constexpr NoThrowForwardIterator2
(ExecutionPolicy&& exec, // see [algorithms.parallel.overloads]
uninitialized_relocate
NoThrowForwardIterator1 first,
NoThrowForwardIterator1 last,);
NoThrowForwardIterator2 result
template<class NoThrowInputIterator, class Size, class NoThrowForwardIterator>
constexpr NoThrowForwardIterator
(NoThrowInputIterator first, // freestanding
uninitialized_relocate_n
Size n,);
NoThrowForwardIterator result
template<class ExecutionPolicy, class NoThrowForwardIterator1, class Size, class NoThrowForwardIterator2>
constexpr NoThrowForwardIterator2
(ExecutionPolicy&& exec, // see [algorithms.parallel.overloads]
uninitialized_relocate_n
NoThrowForwardIterator1 first,
Size n,);
NoThrowForwardIterator2 result
template<class NoThrowBidirectionalIterator1, class NoThrowBidirectionalIterator2>
constexpr NoThrowBidirectionalIterator2
(NoThrowBidirectionalIterator1 first, // freestanding
uninitialized_relocate_backward
NoThrowBidirectionalIterator1 last,);
NoThrowBidirectionalIterator2 result
template<class ExecutionPolicy, class NoThrowBidirectionalIterator1, class NoThrowBidirectionalIterator2>
constexpr NoThrowBidirectionalIterator2
(ExecutionPolicy&& exec, // see [algorithms.parallel.overloads]
uninitialized_relocate_backward
NoThrowBidirectionalIterator1 first,
NoThrowBidirectionalIterator1 last,);
NoThrowBidirectionalIterator2 result
namespace ranges {
template<class I, class O>
using uninitialized_relocate_result = in_out_result<I, O>; // freestanding
template<nothrow-input-iterator I, nothrow-sentinel-for<I> S1,
<O> S2>
nothrow-forward-iterator O, nothrow-sentinel-forrequires relocatable-from<iter_value_t<I>, iter_value_t<O>>
constexpr uninitialized_relocate_result<I, O>
(I ifirst, S1 ilast, O ofirst, S2 olast); // freestanding
uninitialized_relocate
template<nothrow-input-range IR, nothrow-forward_range<I> OR>,
requires relocatable-from<range_value_t<IR>, range_value_t<OR>>
constexpr uninitialized_relocate_result<borrowed_iterator_t<IR>, borrowed_iterator_t<OR>>
(IR&& in_range, OR&& out_range); // freestanding
uninitialized_relocate
template<class I, class O>
using uninitialized_relocate_n_result = in_out_result<I, O>; // freestanding
template<nothrow-input-iterator I,
<O> S>
nothrow-forward-iterator O, nothrow-sentinel-forrequires relocatable-from<iter_value_t<I>, iter_value_t<O>>
constexpr uninitialized_relocate_n_result<I, O>
(I ifirst, iter_difference_t<I> n, O ofirst, S olast); // freestanding
uninitialized_relocate_n
template<class I, class O>
using uninitialized_relocate_backward_result = in_out_result<I, O>; // freestanding
template<nothrow-bidirectional-iterator I, nothrow-sentinel-for<I> S1,
<O> S2>
nothrow-bidirectional-iterator O, nothrow-sentinel-forrequires relocatable-from<iter_value_t<I>, iter_value_t<O>>
constexpr uninitialized_relocate_backward_result<I, O>
(I ifirst, S1 ilast, O ofirst, S2 olast); // freestanding
uninitialized_relocate_backward
template<nothrow-bidirectional-range IR, nothrow-bidirectional-range OR>
requires relocatable-from<range_value_t<IR>, range_value_t<OR>>
constexpr uninitialized_relocate_backward_result<borrowed_iterator_t<IR>, borrowed_iterator_t<OR>>
(IR&& in_range, OR&& out_range); // freestanding
uninitialized_relocate_backward}
Append to 26.11
[specialized.algorithms]
a new subclause,
uninitialized_relocate
[special.relocate], with these contents:
template<class NoThrowInputIterator, class NoThrowForwardIterator>
constexpr NoThrowForwardIterator
(NoThrowInputIterator first,
uninitialized_relocate
NoThrowInputIterator last,); NoThrowForwardIterator result
= result;
NoThrowForwardIterator first_result try {
while (first != last) {
(addressof(*result), addressof(*first));
relocate-at++first;
++result;
}
} catch (...) {
(++first, last);
destroy(first_result, result);
destroythrow;
}
return result;
namespace ranges {
template<nothrow-input-iterator I, nothrow-sentinel-for<I> S1,
<O> S2>
nothrow-forward-iterator O, nothrow-sentinel-forrequires relocatable-from<iter_value_t<I>, iter_value_t<O>>
constexpr uninitialized_relocate_result<I, O>
(I ifirst, S1 ilast, O ofirst, O2 olast);
uninitialized_relocate
template<nothrow-input-range IR, nothrow-forward_range<I> OR>,
requires relocatable-from<range_value_t<IR>, range_value_t<OR>>
constexpr uninitialized_relocate_result<borrowed_iterator_t<IR>, borrowed_iterator_t<OR>>
(IR&& in_range, OR&& out_range);
uninitialized_relocate}
= ofirst;
O first_result try {
while (ifirst != ilast && ofirst != olast) {
(addressof(*ofirst), addressof(*ifirst));
relocate-at++ifirst;
++ofirst;
}
} catch (...) {
(++ifirst, ilast);
destroy(first_result, ofirst);
destroythrow;
}
return {std::move(ifirst), ofirst};
Append to 26.11
[specialized.algorithms]
a new subclause,
uninitialized_relocate_n
[special.relocate_n], with these contents:
template<class NoThrowInputIterator, class Size, class NoThrowForwardIterator>
constexpr NoThrowForwardIterator
(NoThrowInputIterator first,
uninitialized_relocate_n
Size n,); NoThrowForwardIterator result
= result;
NoThrowForwardIterator first_result try {
while (n > 0) {
(addressof(*result), addressof(*first));
relocate-at++first;
++result;
--n;
}
} catch (...) {
(++first, --n);
destroy_n(first_result, result);
destroythrow;
}
return result;
namespace ranges {
template<nothrow-input-iterator I,
<O> S>
nothrow-forward-iterator O, nothrow-sentinel-forrequires relocatable-from<iter_value_t<I>, iter_value_t<O>>
constexpr uninitialized_relocate_n_result<I, O>
(I ifirst, iter_difference_t<I> n, O ofirst, S olast);
uninitialized_relocate_n}
= ofirst;
O first_result try {
while (n > 0 && ofirst != olast) {
(addressof(*ofirst), addressof(*ifirst));
relocate-at++ifirst;
++ofirst;
--n;
}
} catch (...) {
(++ifirst, --n);
destroy_n(first_result, ofirst);
destroythrow;
}
return {std::move(ifirst), ofirst};
Append to 26.11
[specialized.algorithms]
a new subclause,
uninitialized_relocate_backward
[special.relocate_backward], with these contents:
template<class NoThrowBidirectionalIterator1, class NoThrowBidirectionalIterator2>
constexpr NoThrowBidirectionalIterator2
(NoThrowBidirectionalIterator1 first,
uninitialized_relocate_backward
NoThrowBidirectionalIterator1 last,); NoThrowBidirectionalIterator2 result
= result;
NoThrowBidirectionalIterator2 result_last try {
while (last != first) {
--last;
--result;
(addressof(*result), addressof(*last));
relocate-at}
} catch (...) {
(first, last);
destroy(++result, result_last);
destroythrow;
}
return {last, result};
namespace ranges {
template<nothrow-bidirectional-iterator I, nothrow-sentinel-for<I> S1,
<O> S2>
nothrow-bidirectional-iterator O, nothrow-sentinel-forrequires relocatable-from<iter_value_t<I>, iter_value_t<O>>
constexpr uninitialized_relocate_backward_result<I, O>
(I ifirst, S1 ilast, O ofirst, S2 olast);
uninitialized_relocate_backward
template<nothrow-bidirectional-range IR, nothrow-bidirectional-range OR>
requires relocatable-from<range_value_t<IR>, range_value_t<OR>>
constexpr uninitialized_relocate_backward_result<borrowed_iterator_t<IR>, borrowed_iterator_t<OR>>
(IR&& in_range, OR&& out_range);
uninitialized_relocate_backward}
= olast;
O result_last try {
while (ilast != ifirst && olast != ofirst) {
--ilast;
--olast;
(addressof(*olast), addressof(*ilast));
relocate-at}
} catch (...) {
(ifirst, ilast);
destroy(++olast, result_last);
destroythrow;
}
return {ilast, olast};