| 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_iterators (or
bidirectional_iterators 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_iterators 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_backNote 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:
voidifytemplate<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
nothrow-forward-iterator<I> &&
bidirectional_iterator<I> &&
nothrow-sentinel-for<I, I>;bidirectional_iterator
([iterator.concept.bidir]) operations to throw exceptions. — end
note]template<class R>
concept nothrow-bidirectional-range = // exposition only
nothrow-forward-range<R> &&
nothrow-bidirectional-iterator<iterator_t<R>>;In 20.2.2
[memory.syn], add to
the <memory>
header synopsis (before [unique.ptr]):
template<class NoThrowInputIterator, class NoThrowForwardIterator>
constexpr NoThrowForwardIterator
uninitialized_relocate(NoThrowInputIterator first, // freestanding
NoThrowInputIterator last,
NoThrowForwardIterator result);
template<class ExecutionPolicy, class NoThrowForwardIterator1, class NoThrowForwardIterator2>
constexpr NoThrowForwardIterator2
uninitialized_relocate(ExecutionPolicy&& exec, // see [algorithms.parallel.overloads]
NoThrowForwardIterator1 first,
NoThrowForwardIterator1 last,
NoThrowForwardIterator2 result);
template<class NoThrowInputIterator, class Size, class NoThrowForwardIterator>
constexpr NoThrowForwardIterator
uninitialized_relocate_n(NoThrowInputIterator first, // freestanding
Size n,
NoThrowForwardIterator result);
template<class ExecutionPolicy, class NoThrowForwardIterator1, class Size, class NoThrowForwardIterator2>
constexpr NoThrowForwardIterator2
uninitialized_relocate_n(ExecutionPolicy&& exec, // see [algorithms.parallel.overloads]
NoThrowForwardIterator1 first,
Size n,
NoThrowForwardIterator2 result);
template<class NoThrowBidirectionalIterator1, class NoThrowBidirectionalIterator2>
constexpr NoThrowBidirectionalIterator2
uninitialized_relocate_backward(NoThrowBidirectionalIterator1 first, // freestanding
NoThrowBidirectionalIterator1 last,
NoThrowBidirectionalIterator2 result);
template<class ExecutionPolicy, class NoThrowBidirectionalIterator1, class NoThrowBidirectionalIterator2>
constexpr NoThrowBidirectionalIterator2
uninitialized_relocate_backward(ExecutionPolicy&& exec, // see [algorithms.parallel.overloads]
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,
nothrow-forward-iterator O, nothrow-sentinel-for<O> S2>
requires relocatable-from<iter_value_t<I>, iter_value_t<O>>
constexpr uninitialized_relocate_result<I, O>
uninitialized_relocate(I ifirst, S1 ilast, O ofirst, S2 olast); // freestanding
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>>
uninitialized_relocate(IR&& in_range, OR&& out_range); // freestanding
template<class I, class O>
using uninitialized_relocate_n_result = in_out_result<I, O>; // freestanding
template<nothrow-input-iterator I,
nothrow-forward-iterator O, nothrow-sentinel-for<O> S>
requires relocatable-from<iter_value_t<I>, iter_value_t<O>>
constexpr uninitialized_relocate_n_result<I, O>
uninitialized_relocate_n(I ifirst, iter_difference_t<I> n, O ofirst, S olast); // freestanding
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,
nothrow-bidirectional-iterator O, nothrow-sentinel-for<O> S2>
requires relocatable-from<iter_value_t<I>, iter_value_t<O>>
constexpr uninitialized_relocate_backward_result<I, O>
uninitialized_relocate_backward(I ifirst, S1 ilast, O ofirst, S2 olast); // freestanding
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>>
uninitialized_relocate_backward(IR&& in_range, OR&& out_range); // freestanding
}Append to 26.11
[specialized.algorithms]
a new subclause,
uninitialized_relocate
[special.relocate], with these contents:
template<class NoThrowInputIterator, class NoThrowForwardIterator>
constexpr NoThrowForwardIterator
uninitialized_relocate(NoThrowInputIterator first,
NoThrowInputIterator last,
NoThrowForwardIterator result);NoThrowForwardIterator first_result = result;
try {
while (first != last) {
relocate-at(addressof(*result), addressof(*first));
++first;
++result;
}
} catch (...) {
destroy(++first, last);
destroy(first_result, result);
throw;
}
return result;namespace ranges {
template<nothrow-input-iterator I, nothrow-sentinel-for<I> S1,
nothrow-forward-iterator O, nothrow-sentinel-for<O> S2>
requires relocatable-from<iter_value_t<I>, iter_value_t<O>>
constexpr uninitialized_relocate_result<I, O>
uninitialized_relocate(I ifirst, S1 ilast, O ofirst, O2 olast);
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>>
uninitialized_relocate(IR&& in_range, OR&& out_range);
}O first_result = ofirst;
try {
while (ifirst != ilast && ofirst != olast) {
relocate-at(addressof(*ofirst), addressof(*ifirst));
++ifirst;
++ofirst;
}
} catch (...) {
destroy(++ifirst, ilast);
destroy(first_result, ofirst);
throw;
}
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
uninitialized_relocate_n(NoThrowInputIterator first,
Size n,
NoThrowForwardIterator result);NoThrowForwardIterator first_result = result;
try {
while (n > 0) {
relocate-at(addressof(*result), addressof(*first));
++first;
++result;
--n;
}
} catch (...) {
destroy_n(++first, --n);
destroy(first_result, result);
throw;
}
return result;namespace ranges {
template<nothrow-input-iterator I,
nothrow-forward-iterator O, nothrow-sentinel-for<O> S>
requires relocatable-from<iter_value_t<I>, iter_value_t<O>>
constexpr uninitialized_relocate_n_result<I, O>
uninitialized_relocate_n(I ifirst, iter_difference_t<I> n, O ofirst, S olast);
}O first_result = ofirst;
try {
while (n > 0 && ofirst != olast) {
relocate-at(addressof(*ofirst), addressof(*ifirst));
++ifirst;
++ofirst;
--n;
}
} catch (...) {
destroy_n(++ifirst, --n);
destroy(first_result, ofirst);
throw;
}
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
uninitialized_relocate_backward(NoThrowBidirectionalIterator1 first,
NoThrowBidirectionalIterator1 last,
NoThrowBidirectionalIterator2 result);NoThrowBidirectionalIterator2 result_last = result;
try {
while (last != first) {
--last;
--result;
relocate-at(addressof(*result), addressof(*last));
}
} catch (...) {
destroy(first, last);
destroy(++result, result_last);
throw;
}
return {last, result};namespace ranges {
template<nothrow-bidirectional-iterator I, nothrow-sentinel-for<I> S1,
nothrow-bidirectional-iterator O, nothrow-sentinel-for<O> S2>
requires relocatable-from<iter_value_t<I>, iter_value_t<O>>
constexpr uninitialized_relocate_backward_result<I, O>
uninitialized_relocate_backward(I ifirst, S1 ilast, O ofirst, S2 olast);
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>>
uninitialized_relocate_backward(IR&& in_range, OR&& out_range);
}O result_last = olast;
try {
while (ilast != ifirst && olast != ofirst) {
--ilast;
--olast;
relocate-at(addressof(*olast), addressof(*ilast));
}
} catch (...) {
destroy(ifirst, ilast);
destroy(++olast, result_last);
throw;
}
return {ilast, olast};