Skip to content

Commit

Permalink
Remove 'unintialized_relocate', add 'relocate'
Browse files Browse the repository at this point in the history
  • Loading branch information
mclow committed Sep 27, 2024
1 parent 7859fce commit 4cdddbb
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 201 deletions.
67 changes: 65 additions & 2 deletions libcxx/include/__memory/relocate.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define _LIBCPP___MEMORY_RELOCATE_H

#include <__config>
#include <__functional/operations.h>
#include <__type_traits/is_const.h>
#include <__type_traits/is_trivially_relocatable.h>
#include <cstddef>
Expand All @@ -15,9 +16,71 @@ _LIBCPP_BEGIN_NAMESPACE_STD
#if _LIBCPP_STD_VER >= 20

template <class _Tp>
requires(is_trivially_relocatable_v<_Tp> && !is_const_v<_Tp>)
_LIBCPP_EXPORTED_FROM_ABI _Tp* trivially_relocate(_Tp* __begin, _Tp* __end, _Tp* __new_location) noexcept {
return __builtin_trivially_relocate(__new_location, __begin, sizeof(_Tp) * (__end - __begin));
static_assert(is_trivially_relocatable_v<_Tp> && !is_const_v<_Tp>);
(void) __builtin_trivially_relocate(__new_location, __begin, sizeof(_Tp) * (__end - __begin));
return __new_location + (__end - __begin);
}

template <class T>
constexpr
T* relocate(T* __begin, T* __end, T* __new_location)
{
static_assert(is_trivially_relocatable_v<T> || is_nothrow_move_constructible_v<T>);
// When relocating to the same location, do nothing.
if (__begin == __new_location || __begin == __end)
return __new_location + (__end - __begin);

// Then, if we are not evaluating at compile time and the type supports
// trivial relocation, delegate to `trivially_relocate`.
if ! consteval {
if constexpr (is_trivially_relocatable_v<T>)
return std::trivially_relocate(__begin, __end, __new_location);
}

if constexpr (is_move_constructible_v<T>) {
// For nontrivial relocatable types or any time during constant
// evaluation, we must detect overlapping ranges and act accordingly,
// which can be done only if the type is movable.
if ! consteval {
// At run time, when there is no overlap, we can, using other Standard
// Library algorithms, do all moves at once followed by all destructions.
if (less{}(__end,__new_location) || less{}(__new_location + (__end-__begin), __begin)) {
T* result = uninitialized_move(__begin, __end, __new_location);
std::destroy(__begin,__end);
return result;
}
}

if (less{}(__new_location,__begin) || less{}(__end,__new_location)) {
// Any move to a lower address in memory or any nonoverlapping move can be
// done by iterating forward through the range.
T* next = __begin;
T* dest = __new_location;
while (next != __end) {
::new(dest) T(std::move(*next));
next->~T();
++next; ++dest;
}
}
else {
// When moving to a higher address that overlaps, we must go backward through
// the range.
T* next = __end;
T* dest = __new_location + (__end-__begin);
while (next != __begin) {
--next; --dest;
::new(dest) T(std::move(*next));
next->~T();
}
}
return __new_location + (__end - __begin);
}

// The only way to reach this point is during constant evaluation where type `T`
// is trivially relocatable but not move constructible. Such cases are not supported
// so we mark this branch as unreachable.
unreachable();
}

#endif
Expand Down
199 changes: 0 additions & 199 deletions libcxx/include/__memory/uninitialized_algorithms.h
Original file line number Diff line number Diff line change
Expand Up @@ -657,205 +657,6 @@ __uninitialized_allocator_relocate(_Alloc& __alloc, _Tp* __first, _Tp* __last, _
}
}

// Trivial relocation stuff
#if _LIBCPP_STD_VER >= 20

namespace {
// Implementation details --- using an anonymous namespace within this single TU prototype
template <class _Iter>
concept nothrow_input_iterator = input_iterator<_Iter>;

template <class _Iter>
concept nothrow_forward_iterator = forward_iterator<_Iter>;

// We need a concept to distinguish the "not" case at the end --- subsumption
// does not kick in, so could maybe be just a constexpr boolean variable
// template.

template <class _InIter, class _OutIter>
concept relocatable_iterators
= std::same_as<std::iter_value_t<_InIter>, std::iter_value_t<_OutIter>>
and std::is_lvalue_reference_v<std::iter_reference_t<_InIter>>
and std::is_lvalue_reference_v<std::iter_reference_t<_OutIter>>;


template <class _InIter, class _OutIter>
concept no_fail_relocatable_iterators
= relocatable_iterators<_InIter, _OutIter>
and ( std::is_nothrow_move_constructible_v<std::iter_value_t<_InIter>>
or std::is_trivially_relocatable_v<std::iter_value_t<_InIter>>
);
} // unnamed namespace

// Forward looking for asserting preconditions --- tune feature macro as needed
#ifndef __cpp_contracts
# define contract_assert(...) assert(__VA_ARGS__)
#endif


template <contiguous_iterator _InIter, contiguous_iterator _OutIter>
requires no_fail_relocatable_iterators<_InIter, _OutIter>
auto unintialized_relocate(_InIter __first, _InIter __last, _OutIter __result) noexcept
-> _OutIter {
// Note that by design, none of the operations in this implementation are
// potentially-throwing --- hence there are no concerns about exception
// safety

// One implementation bug deferred from proof of concept is that we assume
// that the object pointer type can be implicitly converted to the
// contiguous output iterator when creating the return value. I have yet __result
// confirm that this is mandated by the relevant concepts
// Using pointers to relocate whole range at once, and remove
// no-throw-iterator concerns from support for contiguous iterators. There
// remains corner-case concerns to clean up for throwing `operator*` and
// dereferencing past-the-end iterators for empty ranges.
auto * __begin = std::addressof(*__first);
auto * __end = __begin + (__last - __first);
auto * __new_location = std::addressof(*__result);

// This check is redundant if we follow all the branches below, but makes
// the design intent clear.
if (__begin == __new_location)
return __result;

// For trivially relocatable type, just delegate to the core language primitive
if constexpr (is_trivially_relocatable_v<std::iter_value_t<_InIter>>) {
(void) trivially_relocate(__begin, __end, __new_location);
return __result + (__last - __first);
}

// For non-trivial relocation, we must detect and support overlapping ranges
// ourselves. At this point we no longer need to worry about trivial
// relocatable types but are not move-constructible.

if (__less<>{}(__end, __new_location) || __less<>{}(__new_location + (__end - __begin), __begin)) {
// No overlap
_OutIter __result0 = uninitialized_move(__first, __last, __result);
destroy(__begin, __end);
return __result0;
}
else if (__less<>{}(__begin, __new_location)) {
// move-and-destroy each member, back to front

// Implementation note: we could just modify `__new_location` rather than
// create the local `dest`, but want to clearly show the algorithm
// without micro-optimizing.
auto * __dest = __new_location + (__end - __begin);
while (__dest != __new_location) {
__dest = std::__construct_at(--__dest, std::move(*--__end));
std::__destroy_at(__end);
}
return __result + (__last - __first);

}
else if (__begin == __new_location) {
// Note that this is the same check redundantly hoisted out of the
// if/elif/else flow, but is NOT redundant here
return __result;
}
else {
// move-and-destroy each member, front to back
while (__first != __last) {
(void) __construct_at(std::addressof(*__result), std::move(*__first));
std::__destroy_at(std::addressof(*__first));
++__result;
++__first;
}
return __result;
}

__libcpp_unreachable();
}

template <nothrow_input_iterator _InIter, nothrow_input_iterator _OutIter>
requires no_fail_relocatable_iterators<_InIter, _OutIter>
auto unintialized_relocate(_InIter __first, _InIter __last, _OutIter __result) noexcept
-> _OutIter {
// There is no support for overlapping ranges unless both iterator types are
// contiguous iterators, which would be subsumed by the contiguous itetator
// overload.

// Note that there are trivially relocatable types that are not no-throw
// move constructible and vice-versa, so we need to handle both cases. In
// doing so, we should pick a preferred implementation for types that are
// both trivially relocatable and no-throw move constructible.
if (__first == __last)
return __result;

// Think carefully about iterator invalidation when destroying elements.
// The post-increment on `__first` seems important, in case element
// destruction invalidates an iterator, which would be the case for a
// pointer.
while (__first != __last) {
auto * __begin = std::addressof(*__first++);
auto * __new_location = std::addressof(*__result++);

if constexpr(is_trivially_relocatable_v<std::iter_value_t<_InIter>>) {
(void) trivially_relocate(__begin, __begin + 1, __new_location);
}
else {
(void) std::__construct_at(__new_location, std::move(*__begin));
std::__destroy_at(__begin);
}
}

return __result;
}

template <std::forward_iterator _InIter, nothrow_input_iterator _OutIter>
requires relocatable_iterators<_InIter, _OutIter>
and (!no_fail_relocatable_iterators<_InIter, _OutIter>)
and std::is_move_constructible_v<std::iter_value_t<_InIter>>
auto unintialized_relocate(_InIter __first, _InIter __last, _OutIter __result) // NOT declared noexcept
-> _OutIter {
// The move-constructor can throw, so relocation is not guaranteed to
// succeed. The opt for the vector-like strong exception safety guarantee
// for this operation: if move can throw, but the type is
// copy-constructible, then *copy* the range, so that we can safely unwind
// if an exception is thrown without leaving the initially relocated
// elements in an unknown state. Otherwise, we will *move* all of the
// elements, and leave those elements in an unspecified valid state. Only
// if the copy/move operation succeeds are the original elements destroyed.
if (__first == __last)
return __result;

if constexpr(std::contiguous_iterator<_InIter> and std::contiguous_iterator<_OutIter>) {
// Note that once we complete overload resolution, all viable iterators
// referring to trivially relocatable types should have directed to an
// overload above. We might still have contiguous iterators to
// move-constructible types that may throw, and are not trivially
// relocatable, which means we may be able to assert preconditions that
// overlapping ranges are not supported.
// check for overlapping range with a contract_assert
#if defined(__clang__)
contract_assert(!__is_pointer_in_range(std::addressof(*__first), std::addressof(*__last),
std::addressof(*__result)));
#else
const size_t __count = __last - __first;
auto * __begin = std::addressof(*__first);
auto * __end = __begin + __count; // `__last` is often not a derefencerable iterator
auto * __new_location = std::addressof(*__result);
auto __less_ = std::less<>{};
// Note that __end == __new_location is fine
contract_assert(__less_(__end, __new_location) || __less_(__new_location + __count, __begin));
#endif
}

// Remember original `__result` may be invalidated by this operation, so we need
// to capture the return value.
if constexpr(is_copy_constructible_v<std::iter_value_t<_InIter>>)
__result = std::uninitialized_copy(__first, __last, __result); // cleans up new range if any copy throws
else
__result = std::uninitialized_move(__first, __last, __result); // cleans up new range if any move throws

std::__destroy(__first, __last); // "Library UB" if destructor throws
return __result;
}

// Need to add the ranges stuff here

#endif

_LIBCPP_END_NAMESPACE_STD

_LIBCPP_POP_MACROS
Expand Down

0 comments on commit 4cdddbb

Please sign in to comment.