diff --git a/libs/core/algorithms/include/hpx/parallel/algorithms/uninitialized_relocate.hpp b/libs/core/algorithms/include/hpx/parallel/algorithms/uninitialized_relocate.hpp index 9e00159bba60..7b26c506bd87 100644 --- a/libs/core/algorithms/include/hpx/parallel/algorithms/uninitialized_relocate.hpp +++ b/libs/core/algorithms/include/hpx/parallel/algorithms/uninitialized_relocate.hpp @@ -1,4 +1,5 @@ // Copyright (c) 2014-2023 Hartmut Kaiser +// Copyright (c) 2023 Isidoros Tsaousis-Seiras // // SPDX-License-Identifier: BSL-1.0 // Distributed under the Boost Software License, Version 1.0. (See accompanying @@ -63,7 +64,7 @@ namespace hpx { template FwdIter uninitialized_relocate(InIter1 first, InIter2 last, FwdIter dest); - /// Relocates the elements in the range defined by [first, first + count), to an + /// Relocates the elements in the range defined by [first, last), to an /// uninitialized memory area beginning at \a dest. If an exception is /// thrown during the move-construction of an element, all elements left /// in the input range, as well as all objects already constructed in the @@ -135,6 +136,125 @@ namespace hpx { uninitialized_relocate( ExPolicy&& policy, InIter1 first, InIter2 last, FwdIter dest); + /// Relocates the elements in the range, defined by [first, last), to an + /// uninitialized memory area ending at \a dest_last. The objects are + /// processed in reverse order. If an exception is thrown during the + /// the move-construction of an element, all elements left in the + /// input range, as well as all objects already constructed in the + /// destination range are destroyed. After this algorithm completes, the + /// source range should be freed or reused without destroying the objects. + /// + /// \note Complexity: time: O(n), space: O(1) + /// 1) For "trivially relocatable" underlying types (T) and + /// a contiguous iterator range [first, last): + /// std::distance(first, last)*sizeof(T) bytes are copied. + /// 2) For "trivially relocatable" underlying types (T) and + /// a non-contiguous iterator range [first, last): + /// std::distance(first, last) memory copies of sizeof(T) + /// bytes each are performed. + /// 3) For "non-trivially relocatable" underlying types (T): + /// std::distance(first, last) move assignments and + /// destructions are performed. + /// + /// \note Declare a type as "trivially relocatable" using the + /// `HPX_DECLARE_TRIVIALLY_RELOCATABLE` macros found in + /// . + /// + /// \tparam BiIter1 The type of the source range (deduced). + /// This iterator type must meet the requirements of a + /// Bidirectional iterator. + /// \tparam BiIter2 The type of the iterator representing the + /// destination range (deduced). + /// This iterator type must meet the requirements of a + /// Bidirectional iterator. + /// + /// \param first Refers to the beginning of the sequence of elements + /// the algorithm will be applied to. + /// \param last Refers to the end of the sequence of elements the + /// algorithm will be applied to. + /// \param dest_last Refers to the beginning of the destination range. + /// + /// The assignments in the parallel \a uninitialized_relocate algorithm invoked + /// without an execution policy object will execute in sequential order in + /// the calling thread. + /// + /// \returns The \a uninitialized_relocate_backward algorithm returns \a BiIter2. + /// The \a uninitialized_relocate_backward algorithm returns the + /// bidirectional iterator to the first element in the destination range. + /// + template + BiIter2 uninitialized_relocate_backward( + BiIter1 first, BiIter1 last, BiIter2 dest_last); + + /// Relocates the elements in the range, defined by [first, last), to an + /// uninitialized memory area ending at \a dest_last. The order of the + /// relocation of the objects depends on the execution policy. If an + /// exception is thrown during the the move-construction of an element, + /// all elements left in the input range, as well as all objects already + /// constructed in the destination range are destroyed. After this algorithm + /// completes, the source range should be freed or reused without destroying + /// the objects. + /// + /// \note Using the \a uninitialized_relocate_backward algorithm with the + /// with a non-sequenced execution policy, will not guarantee the + /// order of the relocation of the objects. + /// + /// \note Complexity: time: O(n), space: O(1) + /// 1) For "trivially relocatable" underlying types (T) and + /// a contiguous iterator range [first, last): + /// std::distance(first, last)*sizeof(T) bytes are copied. + /// 2) For "trivially relocatable" underlying types (T) and + /// a non-contiguous iterator range [first, last): + /// std::distance(first, last) memory copies of sizeof(T) + /// bytes each are performed. + /// 3) For "non-trivially relocatable" underlying types (T): + /// std::distance(first, last) move assignments and + /// destructions are performed. + /// + /// \note Declare a type as "trivially relocatable" using the + /// `HPX_DECLARE_TRIVIALLY_RELOCATABLE` macros found in + /// . + /// + /// \tparam ExPolicy The type of the execution policy to use (deduced). + /// It describes the manner in which the execution + /// of the algorithm may be parallelized and the manner + /// in which it executes the assignments. + /// \tparam BiIter1 The type of the source range (deduced). + /// This iterator type must meet the requirements of a + /// Bidirectional iterator. + /// \tparam BiIter2 The type of the iterator representing the + /// destination range (deduced). + /// This iterator type must meet the requirements of a + /// Bidirectional iterator. + /// + /// \param policy The execution policy to use for the scheduling of + /// the iterations. + /// \param first Refers to the beginning of the sequence of elements + /// the algorithm will be applied to. + /// \param last Refers to the end of the sequence of elements the + /// algorithm will be applied to. + /// \param dest_last Refers to the end of the destination range. + /// + /// The assignments in the parallel \a uninitialized_relocate_backward algorithm invoked + /// with an execution policy object of type \a parallel_policy or + /// \a parallel_task_policy are permitted to execute in an + /// unordered fashion in unspecified threads, and indeterminately sequenced + /// within each thread. + /// + /// \returns The \a uninitialized_relocate_backward algorithm returns a + /// \a hpx::future, if the execution policy is of type + /// \a sequenced_task_policy or + /// \a parallel_task_policy and + /// returns \a BiIter2 otherwise. + /// The \a uninitialized_relocate_backward algorithm returns the + /// bidirectional iterator to the first element in the destination + /// range. + /// + template + hpx::parallel::util::detail::algorithm_result + uninitialized_relocate_backward( + ExPolicy&& policy, BiIter1 first, BiIter1 last, BiIter2 dest_last); + /// Relocates the elements in the range, defined by [first, last), to an /// uninitialized memory area beginning at \a dest. If an exception is /// thrown during the move-construction of an element, all elements left @@ -273,8 +393,8 @@ namespace hpx { #include #include #include -#include -#include +#include +#include #include #include @@ -329,11 +449,13 @@ namespace hpx::parallel { InIter part_source = get<0>(iters); FwdIter part_dest = get<1>(iters); - // returns (dest begin, dest end) - return std::make_pair(part_dest, + auto [part_source_advanced, part_dest_advanced] = hpx::experimental::util:: uninitialized_relocate_n_primitive( - part_source, part_size, part_dest)); + part_source, part_size, part_dest); + + // returns (dest begin, dest end) + return std::make_pair(part_dest, part_dest_advanced); }, // finalize, called once if no error occurred [first, dest, count](auto&& data) mutable @@ -382,9 +504,12 @@ namespace hpx::parallel { experimental::util::detail::relocation_traits::is_noexcept_relocatable_v) { - return util::in_out_result{first, + auto [first_advanced, dest_advanced] = hpx::experimental::util::uninitialized_relocate_n_primitive( - first, count, dest)}; + first, count, dest); + + return util::in_out_result{ + first_advanced, dest_advanced}; } // clang-format off @@ -437,11 +562,12 @@ namespace hpx::parallel { FwdIter dest) noexcept(hpx::experimental::util::detail::relocation_traits< InIter1, FwdIter>::is_noexcept_relocatable_v) { - auto count = std::distance(first, last); + auto [first_advanced, dest_advanced] = + hpx::experimental::util::uninitialized_relocate_primitive( + first, last, dest); - return util::in_out_result{first, - hpx::experimental::util::uninitialized_relocate_n_primitive( - first, count, dest)}; + return util::in_out_result{first_advanced, + dest_advanced}; } template + struct uninitialized_relocate_backward + : public algorithm, + IterPair> + { + constexpr uninitialized_relocate_backward() noexcept + : algorithm( + "uninitialized_relocate_backward") + { + } + + // non vectorized overload + template && + hpx::traits::is_bidirectional_iterator_v && + hpx::traits::is_bidirectional_iterator_v + )> + // clang-format on + static util::in_out_result sequential( + ExPolicy&&, BiIter1 first, BiIter1 last, + BiIter2 dest_last) noexcept(hpx::experimental::util::detail:: + relocation_traits::is_noexcept_relocatable_v) + { + auto [last_advanced, dest_last_advanced] = + hpx::experimental::util::uninitialized_relocate_backward_primitive( + first, last, dest_last); + + return util::in_out_result{last_advanced, + dest_last_advanced}; + } + + template && + hpx::traits::is_bidirectional_iterator_v&& + hpx::traits::is_bidirectional_iterator_v + )> + // clang-format on + static util::detail::algorithm_result_t> + parallel(ExPolicy&& policy, BiIter1 first, BiIter1 last, + BiIter2 dest_last) noexcept(hpx::experimental::util::detail:: + relocation_traits::is_noexcept_relocatable_v) + { + auto count = std::distance(first, last); + + auto dest_first = std::prev(dest_last, count); + + return parallel_uninitialized_relocate_n( + HPX_FORWARD(ExPolicy, policy), first, count, dest_first); + } + }; + /// \endcond + } // namespace detail } // namespace hpx::parallel @@ -552,10 +739,37 @@ namespace hpx::experimental { FwdIter>::get(HPX_MOVE(dest)); } + // If running in non-sequenced execution policy, we must check + // that the ranges are not overlapping in the left + if constexpr (!hpx::is_sequenced_execution_policy_v) + { + // if we can check for overlapping ranges + if constexpr (hpx::traits::is_contiguous_iterator_v && + hpx::traits::is_contiguous_iterator_v) + { + auto dest_last = std::next(dest, count); + auto last = std::next(first, count); + // if it is not overlapping in the left direction + if (!((first < dest_last) && (dest_last < last))) + { + // use parallel version + return parallel::util::get_second_element( + hpx::parallel::detail::uninitialized_relocate_n< + parallel::util::in_out_result>() + .call(HPX_FORWARD(ExPolicy, policy), first, + count, dest)); + } + // if it is we continue to use the sequential version + } + // else we assume that the ranges are overlapping, and continue + // to use the sequential version + } + return parallel::util::get_second_element( hpx::parallel::detail::uninitialized_relocate_n< parallel::util::in_out_result>() - .call(HPX_FORWARD(ExPolicy, policy), first, + .call(hpx::execution::seq, first, static_cast(count), dest)); } } uninitialized_relocate_n{}; @@ -638,11 +852,146 @@ namespace hpx::experimental { FwdIter>::get(HPX_MOVE(dest)); } + // If running in non-sequenced execution policy, we must check + // that the ranges are not overlapping in the left + if constexpr (!hpx::is_sequenced_execution_policy_v) + { + // if we can check for overlapping ranges + if constexpr (hpx::traits::is_contiguous_iterator_v && + hpx::traits::is_contiguous_iterator_v) + { + auto dest_last = std::next(dest, count); + // if it is not overlapping in the left direction + if (!((first < dest_last) && (dest_last < last))) + { + // use parallel version + return parallel::util::get_second_element( + hpx::parallel::detail::uninitialized_relocate_n< + parallel::util::in_out_result>() + .call(HPX_FORWARD(ExPolicy, policy), first, + count, dest)); + } + // if it is we continue to use the sequential version + } + // else we assume that the ranges are overlapping, and continue + // to use the sequential version + } + + // sequential execution policy return parallel::util::get_second_element( hpx::parallel::detail::uninitialized_relocate_n< parallel::util::in_out_result>() - .call(HPX_FORWARD(ExPolicy, policy), first, count, dest)); + .call(hpx::execution::seq, first, count, dest)); } } uninitialized_relocate{}; + + /////////////////////////////////////////////////////////////////////////// + // CPO for hpx::uninitialized_relocate_backward + inline constexpr struct uninitialized_relocate_backward_t final + : hpx::detail::tag_parallel_algorithm + { + // clang-format off + template && + hpx::traits::is_iterator_v + )> + // clang-format on + friend BiIter2 tag_fallback_invoke(uninitialized_relocate_backward_t, + BiIter1 first, BiIter1 last, + BiIter2 dest_last) noexcept(util::detail::relocation_traits::is_noexcept_relocatable_v) + { + static_assert(hpx::traits::is_bidirectional_iterator_v && + "The 'first' and 'last' arguments must meet the requirements " + "of bidirectional iterators."); + static_assert(hpx::traits::is_bidirectional_iterator_v, + "The 'dest_last' argument must meet the requirements of a " + "bidirectional iterator."); + static_assert(util::detail::relocation_traits::valid_relocation, + "Relocating from this source type to this destination type is " + "ill-formed"); + // if count is representing a negative value, we do nothing + if (first == last) + { + return dest_last; + } + + return parallel::util::get_second_element( + hpx::parallel::detail::uninitialized_relocate_backward< + parallel::util::in_out_result>() + .call(hpx::execution::seq, first, last, dest_last)); + } + + // clang-format off + template && + hpx::traits::is_iterator_v && + hpx::traits::is_iterator_v + )> + // clang-format on + friend typename hpx::parallel::util::detail::algorithm_result::type + tag_fallback_invoke(uninitialized_relocate_backward_t, + ExPolicy&& policy, BiIter1 first, BiIter1 last, + BiIter2 dest_last) noexcept(util::detail::relocation_traits::is_noexcept_relocatable_v) + { + static_assert(hpx::traits::is_bidirectional_iterator_v, + "The 'first' and 'last' arguments must meet the requirements " + "of bidirectional iterators."); + static_assert(hpx::traits::is_bidirectional_iterator_v, + "The 'dest' argument must meet the requirements of a " + "bidirectional iterator."); + static_assert(util::detail::relocation_traits::valid_relocation, + "Relocating from this source type to this destination type is " + "ill-formed"); + + auto count = std::distance(first, last); + + // if count is representing a negative value, we do nothing + if (hpx::parallel::detail::is_negative(count)) + { + return parallel::util::detail::algorithm_result::get(HPX_MOVE(dest_last)); + } + + // If running in non-sequence execution policy, we must check + // that the ranges are not overlapping in the right + if constexpr (!hpx::is_sequenced_execution_policy_v) + { + // if we can check for overlapping ranges + if constexpr (hpx::traits::is_contiguous_iterator_v && + hpx::traits::is_contiguous_iterator_v) + { + auto dest_first = std::prev(dest_last, count); + // if it is not overlapping in the right direction + if (!((first < dest_first) && (dest_first < last))) + { + // use parallel version + return parallel::util::get_second_element( + hpx::parallel::detail:: + uninitialized_relocate_backward>() + .call(HPX_FORWARD(ExPolicy, policy), first, + last, dest_last)); + } + // if it is we continue to use the sequential version + } + // else we assume that the ranges are overlapping, and continue + // to use the sequential version + } + + // sequential execution policy + return parallel::util::get_second_element( + hpx::parallel::detail::uninitialized_relocate_backward< + parallel::util::in_out_result>() + .call(hpx::execution::seq, first, last, dest_last)); + } + } uninitialized_relocate_backward{}; } // namespace hpx::experimental #endif // DOXYGEN diff --git a/libs/core/algorithms/tests/unit/algorithms/CMakeLists.txt b/libs/core/algorithms/tests/unit/algorithms/CMakeLists.txt index 252c0fcff0a8..3ce6009a1eb0 100644 --- a/libs/core/algorithms/tests/unit/algorithms/CMakeLists.txt +++ b/libs/core/algorithms/tests/unit/algorithms/CMakeLists.txt @@ -146,9 +146,9 @@ set(tests uninitialized_filln uninitialized_move uninitialized_moven + uninitialized_relocate_backward uninitialized_relocate uninitialized_relocaten - uninitialized_relocate_par uninitialized_value_construct uninitialized_value_constructn unique diff --git a/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocate.cpp b/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocate.cpp index 69b7f26e07a0..b90c99a938f0 100644 --- a/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocate.cpp +++ b/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocate.cpp @@ -4,268 +4,518 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +#include #include #include #include -#define N 50 -#define K 10 +#include +#include +#include +#include + +constexpr int N = 500; // number of objects to construct +constexpr int M = 200; // number of objects to relocate +constexpr int K = 100; // number of objects to relocate before throwing + +static_assert(N > M); +static_assert(M > K); using hpx::experimental::is_trivially_relocatable_v; using hpx::experimental::uninitialized_relocate; -struct trivially_relocatable_struct +std::mutex m; + +template +void simple_mutex_operation(F&& f) +{ + std::lock_guard lk(m); + f(); +} + +// enum for the different types of objects +enum relocation_category { - static int count; - static int move_count; - static int dtor_count; + trivially_relocatable, + non_trivially_relocatable, + non_trivially_relocatable_throwing +}; + +template +struct counted_struct +{ + static std::set*> made; + static std::atomic moved; + static std::atomic destroyed; int data; - explicit trivially_relocatable_struct(int data) + explicit counted_struct(int data) : data(data) { - count++; + // Check that we are not constructing an object on top of another + simple_mutex_operation([&]() { + HPX_TEST(!made.count(this)); + made.insert(this); + }); } - trivially_relocatable_struct(trivially_relocatable_struct&& other) + + counted_struct(counted_struct&& other) noexcept( + c != non_trivially_relocatable_throwing) : data(other.data) { - move_count++; - count++; + if constexpr (c == non_trivially_relocatable_throwing) + { + if (moved++ == K - 1) + { + throw 42; + } + } + else + { + moved++; + } + + // Check that we are not constructing an object on top of another + simple_mutex_operation([&]() { + // Unless we are testing overlapping relocation + // we should not be move-constructing an object on top of another + if constexpr (!overlapping_test) + { + HPX_TEST(!made.count(this)); + } + made.insert(this); + }); } - ~trivially_relocatable_struct() + + ~counted_struct() { - dtor_count++; - count--; + destroyed++; + + // Check that the object was constructed + // and not already destroyed + simple_mutex_operation([&]() { + HPX_TEST(made.count(this)); + made.erase(this); + }); } // making sure the address is never directly accessed - friend void operator&(trivially_relocatable_struct) = delete; + friend void operator&(counted_struct) = delete; }; -int trivially_relocatable_struct::count = 0; -int trivially_relocatable_struct::move_count = 0; -int trivially_relocatable_struct::dtor_count = 0; +template +std::set*> + counted_struct::made; + +template +std::atomic counted_struct::moved = 0; + +template +std::atomic counted_struct::destroyed = 0; + +// Non overlapping relocation testing mechanisms +using trivially_relocatable_struct = counted_struct; HPX_DECLARE_TRIVIALLY_RELOCATABLE(trivially_relocatable_struct); -static_assert(is_trivially_relocatable_v); -struct non_trivially_relocatable_struct -{ - static int count; - static int move_count; - static int dtor_count; - int data; +using non_trivially_relocatable_struct = + counted_struct; - explicit non_trivially_relocatable_struct(int data) - : data(data) - { - count++; - } - // mark as noexcept to enter simpler relocation path - non_trivially_relocatable_struct( - non_trivially_relocatable_struct&& other) noexcept - : data(other.data) - { - move_count++; - count++; - } - ~non_trivially_relocatable_struct() - { - dtor_count++; - count--; - } +using non_trivially_relocatable_struct_throwing = + counted_struct; - // making sure the address is never directly accessed - friend void operator&(non_trivially_relocatable_struct) = delete; -}; -int non_trivially_relocatable_struct::count = 0; -int non_trivially_relocatable_struct::move_count = 0; -int non_trivially_relocatable_struct::dtor_count = 0; +// Overlapping relocation testing mechanisms +using trivially_relocatable_struct_overlapping = + counted_struct; +HPX_DECLARE_TRIVIALLY_RELOCATABLE(trivially_relocatable_struct_overlapping); + +using non_trivially_relocatable_struct_overlapping = + counted_struct; + +using non_trivially_relocatable_struct_throwing_overlapping = + counted_struct; + +// Check that the correct types are trivially relocatable +static_assert(is_trivially_relocatable_v); +static_assert( + is_trivially_relocatable_v); static_assert(!is_trivially_relocatable_v); +static_assert( + !is_trivially_relocatable_v); -struct non_trivially_relocatable_struct_throwing +static_assert( + !is_trivially_relocatable_v); +static_assert(!is_trivially_relocatable_v< + non_trivially_relocatable_struct_throwing_overlapping>); + +void clear() { - static int count; - static int move_count; - static int dtor_count; + // Reset for the next test + trivially_relocatable_struct::moved = 0; + trivially_relocatable_struct::destroyed = 0; + trivially_relocatable_struct::made.clear(); + + trivially_relocatable_struct_overlapping::moved = 0; + trivially_relocatable_struct_overlapping::destroyed = 0; + trivially_relocatable_struct_overlapping::made.clear(); + + non_trivially_relocatable_struct::moved = 0; + non_trivially_relocatable_struct::destroyed = 0; + non_trivially_relocatable_struct::made.clear(); + + non_trivially_relocatable_struct_overlapping::moved = 0; + non_trivially_relocatable_struct_overlapping::destroyed = 0; + non_trivially_relocatable_struct_overlapping::made.clear(); + + non_trivially_relocatable_struct_throwing::moved = 0; + non_trivially_relocatable_struct_throwing::destroyed = 0; + non_trivially_relocatable_struct_throwing::made.clear(); + + non_trivially_relocatable_struct_throwing_overlapping::moved = 0; + non_trivially_relocatable_struct_throwing_overlapping::destroyed = 0; + non_trivially_relocatable_struct_throwing_overlapping::made.clear(); +} - int data; +template +std::pair setup() +{ + clear(); - explicit non_trivially_relocatable_struct_throwing(int data) - : data(data) - { - count++; - } - // do not mark as noexcept to enter try-catch relocation path - non_trivially_relocatable_struct_throwing( - non_trivially_relocatable_struct_throwing&& other) - : data(other.data) - { - if (move_count == K) - { - throw 42; - } - move_count++; + void* mem1 = std::malloc(N * sizeof(T)); + void* mem2 = std::malloc(N * sizeof(T)); - count++; - } - ~non_trivially_relocatable_struct_throwing() + HPX_TEST(mem1 && mem2); + + T* ptr1 = static_cast(mem1); + T* ptr2 = static_cast(mem2); + + HPX_TEST(T::made.size() == 0); + HPX_TEST(T::moved == 0); + HPX_TEST(T::destroyed == 0); + + for (int i = 0; i < N; i++) { - dtor_count++; - count--; + hpx::construct_at(ptr1 + i, i); } - // making sure the address is never directly accessed - friend void operator&(non_trivially_relocatable_struct_throwing) = delete; -}; + // fill ptr2 with 0 after M + std::fill(static_cast(mem2) + M * sizeof(T), + static_cast(mem2) + N * sizeof(T), std::byte{0}); -int non_trivially_relocatable_struct_throwing::count = 0; -int non_trivially_relocatable_struct_throwing::move_count = 0; -int non_trivially_relocatable_struct_throwing::dtor_count = 0; + // N objects constructed + HPX_TEST(T::made.size() == N); -static_assert( - !is_trivially_relocatable_v); + return {ptr1, ptr2}; +} -int hpx_main() +template +void test() { - { - void* mem1 = std::malloc(N * sizeof(trivially_relocatable_struct)); - void* mem2 = std::malloc(N * sizeof(trivially_relocatable_struct)); + { // Non-overlapping trivially relocatable + auto [ptr1, ptr2] = setup(); + + for (int i = 0; i < N; i++) + { + // Check that the objects were constructed in the first place + HPX_TEST(trivially_relocatable_struct::made.count(ptr1 + i)); + } - HPX_TEST(mem1 && mem2); + // relocate M objects to ptr2 + uninitialized_relocate(Ex{}, ptr1, ptr1 + M, ptr2); - trivially_relocatable_struct* ptr1 = - static_cast(mem1); - trivially_relocatable_struct* ptr2 = - static_cast(mem2); + // bookkeeping + // Artificially add and remove the objects to the set that are where + // relocated because the relocation is trivial + for (int i = 0; i < M; i++) + { + trivially_relocatable_struct::made.erase(ptr1 + i); + trivially_relocatable_struct::made.insert(ptr2 + i); + } - HPX_TEST(trivially_relocatable_struct::count == 0); - HPX_TEST(trivially_relocatable_struct::move_count == 0); - HPX_TEST(trivially_relocatable_struct::dtor_count == 0); + // No move constructor or destructor should be called + HPX_TEST(trivially_relocatable_struct::moved == 0); + HPX_TEST(trivially_relocatable_struct::destroyed == 0); - for (int i = 0; i < N; i++) + // Objects not touched + for (int i = M; i < N; i++) + { + HPX_TEST(ptr1[i].data == i); + } + + // Objects moved from ptr1 to ptr2 + for (int i = 0; i < M; i++) + { + HPX_TEST(ptr2[i].data == i); + } + + // make sure the memory beyond M is untouched + for (std::byte* p = reinterpret_cast(ptr2 + M); + p < reinterpret_cast(ptr2 + N); p++) { - hpx::construct_at(ptr1 + i, 1234); + HPX_TEST(*p == std::byte{0}); } - // N objects constructed - HPX_TEST(trivially_relocatable_struct::count == N); + // From our perspective, the objects in ptr1 are destroyed + std::destroy(ptr1 + M, ptr1 + N); + std::destroy(ptr2, ptr2 + M); + + HPX_TEST(trivially_relocatable_struct::made.empty()); + + std::free(ptr1); + std::free(ptr2); + } + { // Non-overlapping non-trivially relocatable + auto [ptr1, ptr2] = setup(); // relocate them to ptr2 - uninitialized_relocate(ptr1, ptr1 + N, ptr2); + uninitialized_relocate(Ex{}, ptr1, ptr1 + M, ptr2); - // All creations - destructions balance out - HPX_TEST(trivially_relocatable_struct::count == N); + // M move constructors were called and M destructors + HPX_TEST(non_trivially_relocatable_struct::moved == M); + HPX_TEST(non_trivially_relocatable_struct::destroyed == M); - // No move constructor or destructor should be called - HPX_TEST(trivially_relocatable_struct::move_count == 0); - HPX_TEST(trivially_relocatable_struct::dtor_count == 0); + // Objects not touched + for (int i = M; i < N; i++) + { + HPX_TEST(ptr1[i].data == i); + } - for (int i = 0; i < N; i++) + // Objects moved from ptr1 to ptr2 + for (int i = 0; i < M; i++) { - HPX_TEST(ptr2[i].data == 1234); + HPX_TEST(ptr2[i].data == i); } - std::destroy(ptr2, ptr2 + N); + // make sure the memory beyond M is untouched + for (std::byte* p = reinterpret_cast(ptr2 + M); + p < reinterpret_cast(ptr2 + N); p++) + { + HPX_TEST(*p == std::byte{0}); + } + + std::destroy(ptr1 + M, ptr1 + N); + std::destroy(ptr2, ptr2 + M); + + HPX_TEST(non_trivially_relocatable_struct::made.empty()); - std::free(mem1); - std::free(mem2); + std::free(ptr1); + std::free(ptr2); } - { - void* mem1 = std::malloc(N * sizeof(non_trivially_relocatable_struct)); - void* mem2 = std::malloc(N * sizeof(non_trivially_relocatable_struct)); + { // Non-overlapping non-trivially relocatable throwing + auto [ptr1, ptr2] = setup(); - HPX_TEST(mem1 && mem2); + // relocate M objects to ptr2 + try + { + uninitialized_relocate(Ex{}, ptr1, ptr1 + M, ptr2); + HPX_UNREACHABLE; // should have thrown + } + catch (...) + { + } - non_trivially_relocatable_struct* ptr1 = - static_cast(mem1); - non_trivially_relocatable_struct* ptr2 = - static_cast(mem2); + // If the order is sequenced we can guarantee that no moving + // occurs after the exception is thrown + if constexpr (std::is_same_v) + { + // K move constructors were called, and then the last one throws + HPX_TEST(non_trivially_relocatable_struct_throwing::moved == K); + + // K - 1 objects where destroyed after being moved-from + // and then M destructors were called: K on the old range and + // M - K on the new range + HPX_TEST(non_trivially_relocatable_struct_throwing::destroyed == + K - 1 + M); + } - HPX_TEST(non_trivially_relocatable_struct::count == 0); - HPX_TEST(non_trivially_relocatable_struct::move_count == 0); - HPX_TEST(non_trivially_relocatable_struct::dtor_count == 0); + // After the exception is caught, all the objects in the old and the new + // range are destroyed, so M objects are destroyed on each range + // In the case of parallel execution, the number of moves done before stopping + // is arbitrary, since the exception in one thread is not guaranteed to stop + // all the other threads. + HPX_TEST( + non_trivially_relocatable_struct_throwing::made.size() == N - M); - for (int i = 0; i < N; i++) + // The objects in the ends of ptr1 are still valid + for (int i = M; i < N; i++) { - hpx::construct_at(ptr1 + i, 1234); + HPX_TEST(ptr1[i].data == i); } - // N objects constructed - HPX_TEST(non_trivially_relocatable_struct::count == N); + // make sure the memory beyond M is untouched + for (std::byte* p = reinterpret_cast(ptr2 + M); + p < reinterpret_cast(ptr2 + N); p++) + { + HPX_TEST(*p == std::byte{0}); + } - // relocate them to ptr2 - uninitialized_relocate(ptr1, ptr1 + N, ptr2); + std::destroy(ptr1 + M, ptr1 + N); - // All creations - destructions balance out - HPX_TEST(non_trivially_relocatable_struct::count == N); + HPX_TEST(non_trivially_relocatable_struct_throwing::made.empty()); - // Every object was moved from and then destroyed - HPX_TEST(non_trivially_relocatable_struct::move_count == N); - HPX_TEST(non_trivially_relocatable_struct::dtor_count == N); + std::free(ptr1); + std::free(ptr2); + } + clear(); - for (int i = 0; i < N; i++) + return; +} + +template +void test_overlapping() +{ + // using Ex = hpx::execution::sequenced_policy; + // static_assert(std::is_same_v); + + constexpr int offset = 4; + + // relocating M objects `offset` positions forward + static_assert(M + offset <= N); + + { // Overlapping trivially-relocatable + auto [ptr, ___] = setup(); + + // Destroy the objects that will be overwritten for bookkeeping + std::destroy(ptr, ptr + offset); + HPX_TEST(trivially_relocatable_struct_overlapping::destroyed == offset); + + // relocate M objects `offset` positions backwards + uninitialized_relocate(Ex{}, ptr + offset, ptr + M + offset, ptr); + + // Artificially remove the objects from the set for bookkeeping + for (int i = offset; i < M + offset; i++) + { + trivially_relocatable_struct_overlapping::made.erase(ptr + i); + } // and add the objects that were relocated + for (int i = 0; i < M; i++) + { + trivially_relocatable_struct_overlapping::made.insert(ptr + i); + } + + // No move constructor or destructor should be called + // because the objects are trivially relocatable + HPX_TEST(trivially_relocatable_struct_overlapping::moved == 0); + HPX_TEST(trivially_relocatable_struct_overlapping::destroyed == offset); + + // Objects relocated backwards + for (int i = 0; i < M; i++) { - HPX_TEST(ptr2[i].data == 1234); + HPX_TEST(ptr[i].data == i + offset); } - std::destroy(ptr2, ptr2 + N); + // Objects not touched + for (int i = M + offset; i < N; i++) + { + HPX_TEST(ptr[i].data == i); + } - std::free(mem1); - std::free(mem2); + // Destroy objects within their lifetime + // from our perspective objects in the range [M, M + offset) are destroyed + std::destroy(ptr, ptr + M); + std::destroy(ptr + M + offset, ptr + N); + + HPX_TEST(trivially_relocatable_struct_overlapping::made.empty()); + + std::free(ptr); } - { - void* mem1 = - std::malloc(N * sizeof(non_trivially_relocatable_struct_throwing)); - void* mem2 = - std::malloc(N * sizeof(non_trivially_relocatable_struct_throwing)); + { // Overlapping non-trivially relocatable + auto [ptr, ___] = setup(); - HPX_TEST(mem1 && mem2); + // Destroy the objects that will be overwritten for bookkeeping purposes + std::destroy(ptr, ptr + offset); + HPX_TEST( + non_trivially_relocatable_struct_overlapping::destroyed == offset); - non_trivially_relocatable_struct_throwing* ptr1 = - static_cast(mem1); - non_trivially_relocatable_struct_throwing* ptr2 = - static_cast(mem2); + // relocate backwards them to ptr2 + uninitialized_relocate(Ex{}, ptr + offset, ptr + M + offset, ptr); - HPX_TEST(non_trivially_relocatable_struct_throwing::count == 0); - HPX_TEST(non_trivially_relocatable_struct_throwing::move_count == 0); - HPX_TEST(non_trivially_relocatable_struct_throwing::dtor_count == 0); + // M move constructors were called and M destructors + prior destructors + HPX_TEST(non_trivially_relocatable_struct_overlapping::moved == M); + HPX_TEST(non_trivially_relocatable_struct_overlapping::destroyed == + M + offset); - for (int i = 0; i < N; i++) + // Objects relocated forwards + for (int i = 0; i < M; i++) { - hpx::construct_at(ptr1 + i, 1234); + HPX_TEST(ptr[i].data == i + offset); } - // N objects constructed - HPX_TEST(non_trivially_relocatable_struct_throwing::count == N); + // Objects not touched + for (int i = M + offset; i < N; i++) + { + HPX_TEST(ptr[i].data == i); + } - // relocate them to ptr2 + // Destroy objects within their lifetime + // objects in the range [M, M + offset) are destroyed + std::destroy(ptr, ptr + M); + std::destroy(ptr + M + offset, ptr + N); + + HPX_TEST(non_trivially_relocatable_struct_overlapping::made.empty()); + + std::free(ptr); + } + { // Overlapping non-trivially relocatable throwing + auto [ptr, ___] = + setup(); + + // Destroy the objects that will be overwritten for bookkeeping purposes + std::destroy(ptr, ptr + offset); + HPX_TEST( + non_trivially_relocatable_struct_throwing_overlapping::destroyed == + offset); + + // relocate them backwards try { - uninitialized_relocate(ptr1, ptr1 + N, ptr2); + uninitialized_relocate(Ex{}, ptr + offset, ptr + M + offset, ptr); HPX_UNREACHABLE; // should have thrown } catch (...) { } - // K move constructors were called - HPX_TEST(non_trivially_relocatable_struct_throwing::move_count == K); + // Because we know the execution is sequenced: - // K - 1 destructors were called to balance out the move constructors - // (- 1 because the last move constructor throws) - // and then N + 1 destructors were called: K on the old range and - // N - (K - 1) = N - K + 1 on the new range + // K move constructors were called, and then the last one throws HPX_TEST( - non_trivially_relocatable_struct_throwing::dtor_count == N + K); + non_trivially_relocatable_struct_throwing_overlapping::moved == K); + + // K - 1 objects where destroyed after being moved-from + // and then M destructors were called: K on the new range and + // M - K on the old range + // + offset from prior the relocation + HPX_TEST( + non_trivially_relocatable_struct_throwing_overlapping::destroyed == + K - 1 + M + offset); + + // The objects in the end of ptr1 are still valid + for (int i = M + offset; i < N; i++) + { + HPX_TEST(ptr[i].data == i); + } + + // Destroy objects within their lifetime + std::destroy(ptr + M + offset, ptr + N); - // It stops at K, so K-1 move-destruct pairs have been executed - // after this N - (K - 1) destructs will be done on the old range - // and K - 1 on the new range. giving 2*N total destructs + HPX_TEST(non_trivially_relocatable_struct_throwing_overlapping::made + .empty()); - std::free(mem1); - std::free(mem2); + std::free(ptr); } + clear(); + + return; +} + +int hpx_main() +{ + test(); + test(); + + test_overlapping(); + test_overlapping(); + return hpx::local::finalize(); } diff --git a/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocate_backward.cpp b/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocate_backward.cpp new file mode 100644 index 000000000000..cd787db3e2ad --- /dev/null +++ b/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocate_backward.cpp @@ -0,0 +1,525 @@ +// Copyright (c) 2023 Isidoros Tsaousis-Seiras +// +// SPDX-License-Identifier: BSL-1.0 +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include +#include +#include + +#include +#include +#include +#include + +constexpr int N = 500; // number of objects to construct +constexpr int M = 200; // number of objects to relocate +constexpr int K = 100; // number of objects to relocate before throwing + +static_assert(N > M); +static_assert(M > K); + +using hpx::experimental::is_trivially_relocatable_v; +using hpx::experimental::uninitialized_relocate_backward; + +std::mutex m; + +template +void simple_mutex_operation(F&& f) +{ + std::lock_guard lk(m); + f(); +} + +// enum for the different types of objects +enum relocation_category +{ + trivially_relocatable, + non_trivially_relocatable, + non_trivially_relocatable_throwing +}; + +template +struct counted_struct +{ + static std::set*> made; + static std::atomic moved; + static std::atomic destroyed; + int data; + + explicit counted_struct(int data) + : data(data) + { + // Check that we are not constructing an object on top of another + simple_mutex_operation([&]() { + HPX_TEST(!made.count(this)); + made.insert(this); + }); + } + + counted_struct(counted_struct&& other) noexcept( + c != non_trivially_relocatable_throwing) + : data(other.data) + { + if constexpr (c == non_trivially_relocatable_throwing) + { + if (moved++ == K - 1) + { + throw 42; + } + } + else + { + moved++; + } + + // Check that we are not constructing an object on top of another + simple_mutex_operation([&]() { + // Unless we are testing overlapping relocation + // we should not be move-constructing an object on top of another + if constexpr (!overlapping_test) + { + HPX_TEST(!made.count(this)); + } + made.insert(this); + }); + } + + ~counted_struct() + { + destroyed++; + + // Check that the object was constructed + // and not already destroyed + simple_mutex_operation([&]() { + HPX_TEST(made.count(this)); + made.erase(this); + }); + } + + // making sure the address is never directly accessed + friend void operator&(counted_struct) = delete; +}; + +template +std::set*> + counted_struct::made; + +template +std::atomic counted_struct::moved = 0; + +template +std::atomic counted_struct::destroyed = 0; + +// Non overlapping relocation testing mechanisms +using trivially_relocatable_struct = counted_struct; +HPX_DECLARE_TRIVIALLY_RELOCATABLE(trivially_relocatable_struct); + +using non_trivially_relocatable_struct = + counted_struct; + +using non_trivially_relocatable_struct_throwing = + counted_struct; + +// Overlapping relocation testing mechanisms +using trivially_relocatable_struct_overlapping = + counted_struct; +HPX_DECLARE_TRIVIALLY_RELOCATABLE(trivially_relocatable_struct_overlapping); + +using non_trivially_relocatable_struct_overlapping = + counted_struct; + +using non_trivially_relocatable_struct_throwing_overlapping = + counted_struct; + +// Check that the correct types are trivially relocatable +static_assert(is_trivially_relocatable_v); +static_assert( + is_trivially_relocatable_v); + +static_assert(!is_trivially_relocatable_v); +static_assert( + !is_trivially_relocatable_v); + +static_assert( + !is_trivially_relocatable_v); +static_assert(!is_trivially_relocatable_v< + non_trivially_relocatable_struct_throwing_overlapping>); + +void clear() +{ + // Reset for the next test + trivially_relocatable_struct::moved = 0; + trivially_relocatable_struct::destroyed = 0; + trivially_relocatable_struct::made.clear(); + + trivially_relocatable_struct_overlapping::moved = 0; + trivially_relocatable_struct_overlapping::destroyed = 0; + trivially_relocatable_struct_overlapping::made.clear(); + + non_trivially_relocatable_struct::moved = 0; + non_trivially_relocatable_struct::destroyed = 0; + non_trivially_relocatable_struct::made.clear(); + + non_trivially_relocatable_struct_overlapping::moved = 0; + non_trivially_relocatable_struct_overlapping::destroyed = 0; + non_trivially_relocatable_struct_overlapping::made.clear(); + + non_trivially_relocatable_struct_throwing::moved = 0; + non_trivially_relocatable_struct_throwing::destroyed = 0; + non_trivially_relocatable_struct_throwing::made.clear(); + + non_trivially_relocatable_struct_throwing_overlapping::moved = 0; + non_trivially_relocatable_struct_throwing_overlapping::destroyed = 0; + non_trivially_relocatable_struct_throwing_overlapping::made.clear(); +} + +template +std::pair setup() +{ + clear(); + + void* mem1 = std::malloc(N * sizeof(T)); + void* mem2 = std::malloc(N * sizeof(T)); + + HPX_TEST(mem1 && mem2); + + T* ptr1 = static_cast(mem1); + T* ptr2 = static_cast(mem2); + + HPX_TEST(T::made.size() == 0); + HPX_TEST(T::moved == 0); + HPX_TEST(T::destroyed == 0); + + for (int i = 0; i < N; i++) + { + hpx::construct_at(ptr1 + i, i); + } + + // fill ptr2 with 0 after M + std::fill(static_cast(mem2) + M * sizeof(T), + static_cast(mem2) + N * sizeof(T), std::byte{0}); + + // N objects constructed + HPX_TEST(T::made.size() == N); + + return {ptr1, ptr2}; +} + +template +void test() +{ + { // Non-overlapping trivially relocatable + auto [ptr1, ptr2] = setup(); + + for (int i = 0; i < N; i++) + { + // Check that the objects were constructed in the first place + HPX_TEST(trivially_relocatable_struct::made.count(ptr1 + i)); + } + + // relocate M objects to ptr2 + uninitialized_relocate_backward(Ex{}, ptr1, ptr1 + M, ptr2 + M); + + // bookkeeping + // Artificially add and remove the objects to the set that are where + // relocated because the relocation is trivial + for (int i = 0; i < M; i++) + { + trivially_relocatable_struct::made.erase(ptr1 + i); + trivially_relocatable_struct::made.insert(ptr2 + i); + } + + // No move constructor or destructor should be called + HPX_TEST(trivially_relocatable_struct::moved == 0); + HPX_TEST(trivially_relocatable_struct::destroyed == 0); + + // Objects not touched + for (int i = M; i < N; i++) + { + HPX_TEST(ptr1[i].data == i); + } + + // Objects moved from ptr1 to ptr2 + for (int i = 0; i < M; i++) + { + HPX_TEST(ptr2[i].data == i); + } + + // make sure the memory beyond M is untouched + for (std::byte* p = reinterpret_cast(ptr2 + M); + p < reinterpret_cast(ptr2 + N); p++) + { + HPX_TEST(*p == std::byte{0}); + } + + // From our perspective, the objects in ptr1 are destroyed + std::destroy(ptr1 + M, ptr1 + N); + std::destroy(ptr2, ptr2 + M); + + HPX_TEST(trivially_relocatable_struct::made.empty()); + + std::free(ptr1); + std::free(ptr2); + } + { // Non-overlapping non-trivially relocatable + auto [ptr1, ptr2] = setup(); + + // relocate them to ptr2 + uninitialized_relocate_backward(Ex{}, ptr1, ptr1 + M, ptr2 + M); + + // M move constructors were called and M destructors + HPX_TEST(non_trivially_relocatable_struct::moved == M); + HPX_TEST(non_trivially_relocatable_struct::destroyed == M); + + // Objects not touched + for (int i = M; i < N; i++) + { + HPX_TEST(ptr1[i].data == i); + } + + // Objects moved from ptr1 to ptr2 + for (int i = 0; i < M; i++) + { + HPX_TEST(ptr2[i].data == i); + } + + // make sure the memory beyond M is untouched + for (std::byte* p = reinterpret_cast(ptr2 + M); + p < reinterpret_cast(ptr2 + N); p++) + { + HPX_TEST(*p == std::byte{0}); + } + + std::destroy(ptr1 + M, ptr1 + N); + std::destroy(ptr2, ptr2 + M); + + HPX_TEST(non_trivially_relocatable_struct::made.empty()); + + std::free(ptr1); + std::free(ptr2); + } + { // Non-overlapping non-trivially relocatable throwing + auto [ptr1, ptr2] = setup(); + + // relocate M objects to ptr2 + try + { + uninitialized_relocate_backward(Ex{}, ptr1, ptr1 + M, ptr2 + M); + HPX_UNREACHABLE; // should have thrown + } + catch (...) + { + } + + // If the order is sequenced we can guarantee that no moving + // occurs after the exception is thrown + if constexpr (std::is_same_v) + { + // K move constructors were called, and then the last one throws + HPX_TEST(non_trivially_relocatable_struct_throwing::moved == K); + + // K - 1 objects where destroyed after being moved-from + // and then M destructors were called: K on the old range and + // M - K on the new range + HPX_TEST(non_trivially_relocatable_struct_throwing::destroyed == + K - 1 + M); + } + + // After the exception is caught, all the objects in the old and the new + // range are destroyed, so M objects are destroyed on each range + // In the case of parallel execution, the number of moves done before stopping + // is arbitrary, since the exception in one thread is not guaranteed to stop + // all the other threads. + HPX_TEST( + non_trivially_relocatable_struct_throwing::made.size() == N - M); + + // The objects in the ends of ptr1 are still valid + for (int i = M; i < N; i++) + { + HPX_TEST(ptr1[i].data == i); + } + + // make sure the memory beyond M is untouched + for (std::byte* p = reinterpret_cast(ptr2 + M); + p < reinterpret_cast(ptr2 + N); p++) + { + HPX_TEST(*p == std::byte{0}); + } + + std::destroy(ptr1 + M, ptr1 + N); + + HPX_TEST(non_trivially_relocatable_struct_throwing::made.empty()); + + std::free(ptr1); + std::free(ptr2); + } + clear(); + + return; +} + +template +void test_overlapping() +{ + // using Ex = hpx::execution::sequenced_policy; + // static_assert(std::is_same_v); + + constexpr int offset = 4; + + // relocating M objects `offset` positions forward + static_assert(M + offset <= N); + + { // Overlapping trivially-relocatable + auto [ptr, ___] = setup(); + + // Destroy the objects that will be overwritten for bookkeeping + std::destroy(ptr + M, ptr + M + offset); + HPX_TEST(trivially_relocatable_struct_overlapping::destroyed == offset); + + // relocate M objects `offset` positions forwards + uninitialized_relocate_backward(Ex{}, ptr, ptr + M, ptr + M + offset); + + // Artificially remove the objects from the set for bookkeeping + for (int i = 0; i < M; i++) + { + trivially_relocatable_struct_overlapping::made.erase(ptr + i); + } // and add the objects that were relocated + for (int i = offset; i < M + offset; i++) + { + trivially_relocatable_struct_overlapping::made.insert(ptr + i); + } + + // No move constructor or destructor should be called + // because the objects are trivially relocatable + HPX_TEST(trivially_relocatable_struct_overlapping::moved == 0); + HPX_TEST(trivially_relocatable_struct_overlapping::destroyed == offset); + + // Objects relocated forwards + for (int i = offset; i < M + offset; i++) + { + HPX_TEST(ptr[i].data == i - offset); + } + + // Objects not touched + for (int i = M + offset; i < N; i++) + { + HPX_TEST(ptr[i].data == i); + } + + // Destroy objects within their lifetime + // from our perspective objects in the range [M, M + offset) are destroyed + std::destroy(ptr + offset, ptr + N); + + HPX_TEST(trivially_relocatable_struct_overlapping::made.empty()); + + std::free(ptr); + } + { // Overlapping non-trivially relocatable + auto [ptr, ___] = setup(); + + // Destroy the objects that will be overwritten for bookkeeping purposes + std::destroy(ptr + M, ptr + M + offset); + HPX_TEST( + non_trivially_relocatable_struct_overlapping::destroyed == offset); + + // relocate backwards them to ptr2 + uninitialized_relocate_backward(Ex{}, ptr, ptr + M, ptr + M + offset); + + // M move constructors were called and M destructors + prior destructors + HPX_TEST(non_trivially_relocatable_struct_overlapping::moved == M); + HPX_TEST(non_trivially_relocatable_struct_overlapping::destroyed == + M + offset); + + // Objects relocated forwards + for (int i = offset; i < M + offset; i++) + { + HPX_TEST(ptr[i].data == i - offset); + } + + // Objects not touched + for (int i = M + offset; i < N; i++) + { + HPX_TEST(ptr[i].data == i); + } + + // Destroy objects within their lifetime + // objects in the range [M, M + offset) are destroyed + std::destroy(ptr + offset, ptr + N); + + HPX_TEST(non_trivially_relocatable_struct_overlapping::made.empty()); + + std::free(ptr); + } + { // Overlapping non-trivially relocatable throwing + auto [ptr, ___] = + setup(); + + // Destroy the objects that will be overwritten for bookkeeping purposes + std::destroy(ptr + M, ptr + M + offset); + HPX_TEST( + non_trivially_relocatable_struct_throwing_overlapping::destroyed == + offset); + + // relocate them backwards + try + { + uninitialized_relocate_backward( + Ex{}, ptr, ptr + M, ptr + M + offset); + HPX_UNREACHABLE; // should have thrown + } + catch (...) + { + } + + // Because we know the execution is sequenced: + + // K move constructors were called, and then the last one throws + HPX_TEST( + non_trivially_relocatable_struct_throwing_overlapping::moved == K); + + // K - 1 objects where destroyed after being moved-from + // and then M destructors were called: K on the new range and + // M - K on the old range + // + offset from prior the relocation + HPX_TEST( + non_trivially_relocatable_struct_throwing_overlapping::destroyed == + K - 1 + M + offset); + + // The objects in the end of ptr1 are still valid + for (int i = M + offset; i < N; i++) + { + HPX_TEST(ptr[i].data == i); + } + + // Destroy objects within their lifetime + std::destroy(ptr + M + offset, ptr + N); + + HPX_TEST(non_trivially_relocatable_struct_throwing_overlapping::made + .empty()); + + std::free(ptr); + } + clear(); + + return; +} + +int hpx_main() +{ + test(); + test(); + + test_overlapping(); + test_overlapping(); + + return hpx::local::finalize(); +} + +int main(int argc, char* argv[]) +{ + hpx::local::init(hpx_main, argc, argv); + return hpx::util::report_errors(); +} diff --git a/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocate_par.cpp b/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocate_par.cpp deleted file mode 100644 index 47d402aea59a..000000000000 --- a/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocate_par.cpp +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright (c) 2023 Isidoros Tsaousis-Seiras -// -// SPDX-License-Identifier: BSL-1.0 -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) - -#include -#include -#include -#include -#include - -#define N 5000 -#define K 10 - -using hpx::experimental::is_trivially_relocatable_v; -using hpx::experimental::uninitialized_relocate; - -struct trivially_relocatable_struct -{ - static std::atomic count; - static std::atomic move_count; - static std::atomic dtor_count; - int data; - - explicit trivially_relocatable_struct(int data) - : data(data) - { - count.fetch_add(1); - } - trivially_relocatable_struct(trivially_relocatable_struct&& other) - : data(other.data) - { - move_count.fetch_add(1); - count++; - } - ~trivially_relocatable_struct() - { - dtor_count.fetch_add(1); - count--; - } - - // making sure the address is never directly accessed - friend void operator&(trivially_relocatable_struct) = delete; -}; -std::atomic trivially_relocatable_struct::count = 0; -std::atomic trivially_relocatable_struct::move_count = 0; -std::atomic trivially_relocatable_struct::dtor_count = 0; - -HPX_DECLARE_TRIVIALLY_RELOCATABLE(trivially_relocatable_struct); -static_assert(is_trivially_relocatable_v); - -struct non_trivially_relocatable_struct -{ - static std::atomic count; - static std::atomic move_count; - static std::atomic dtor_count; - int data; - - explicit non_trivially_relocatable_struct(int data) - : data(data) - { - count++; - } - // mark as noexcept to enter simpler relocation path - non_trivially_relocatable_struct( - non_trivially_relocatable_struct&& other) noexcept - : data(other.data) - { - move_count.fetch_add(1); - count++; - } - ~non_trivially_relocatable_struct() - { - dtor_count.fetch_add(1); - count--; - } - - // making sure the address is never directly accessed - friend void operator&(non_trivially_relocatable_struct) = delete; -}; -std::atomic non_trivially_relocatable_struct::count = 0; -std::atomic non_trivially_relocatable_struct::move_count = 0; -std::atomic non_trivially_relocatable_struct::dtor_count = 0; - -static_assert(!is_trivially_relocatable_v); - -struct non_trivially_relocatable_struct_throwing -{ - static std::atomic count; - static std::atomic move_count; - static std::atomic dtor_count; - - int data; - - explicit non_trivially_relocatable_struct_throwing(int data) - : data(data) - { - count++; - } - // do not mark as noexcept to enter try-catch relocation path - non_trivially_relocatable_struct_throwing( - non_trivially_relocatable_struct_throwing&& other) - : data(other.data) - { - if (move_count.fetch_add(1) == K) - { - throw 42; - } - - count++; - } - ~non_trivially_relocatable_struct_throwing() - { - dtor_count.fetch_add(1); - count--; - } - - // making sure the address is never directly accessed - friend void operator&(non_trivially_relocatable_struct_throwing) = delete; -}; - -std::atomic non_trivially_relocatable_struct_throwing::count = 0; -std::atomic non_trivially_relocatable_struct_throwing::move_count = 0; -std::atomic non_trivially_relocatable_struct_throwing::dtor_count = 0; - -static_assert( - !is_trivially_relocatable_v); - -char msg[256]; - -int hpx_main() -{ - { - void* mem1 = std::malloc(N * sizeof(trivially_relocatable_struct)); - void* mem2 = std::malloc(N * sizeof(trivially_relocatable_struct)); - - HPX_TEST(mem1 && mem2); - - trivially_relocatable_struct* ptr1 = - static_cast(mem1); - trivially_relocatable_struct* ptr2 = - static_cast(mem2); - - HPX_TEST(trivially_relocatable_struct::count.load() == 0); - HPX_TEST(trivially_relocatable_struct::move_count.load() == 0); - HPX_TEST(trivially_relocatable_struct::dtor_count.load() == 0); - - for (int i = 0; i < N; i++) - { - hpx::construct_at(ptr1 + i, 1234); - } - - // N objects constructed - HPX_TEST(trivially_relocatable_struct::count.load() == N); - - // relocate them to ptr2 - uninitialized_relocate(hpx::execution::par, ptr1, ptr1 + N, ptr2); - - sprintf(msg, "count: %d, move_count: %d, dtor_count: %d", - trivially_relocatable_struct::count.load(), - trivially_relocatable_struct::move_count.load(), - trivially_relocatable_struct::dtor_count.load()); - - // All creations - destructions balance out - HPX_TEST_MSG(trivially_relocatable_struct::count.load() == N, msg); - - // No move constructor or destructor should be called - HPX_TEST_MSG(trivially_relocatable_struct::move_count.load() == 0, msg); - HPX_TEST_MSG(trivially_relocatable_struct::dtor_count.load() == 0, msg); - - for (int i = 0; i < N; i++) - { - HPX_TEST(ptr2[i].data == 1234); - } - - std::destroy_n(ptr2, N); - - std::free(mem1); - std::free(mem2); - } - { - void* mem1 = std::malloc(N * sizeof(non_trivially_relocatable_struct)); - void* mem2 = std::malloc(N * sizeof(non_trivially_relocatable_struct)); - - HPX_TEST(mem1 && mem2); - - non_trivially_relocatable_struct* ptr1 = - static_cast(mem1); - non_trivially_relocatable_struct* ptr2 = - static_cast(mem2); - - HPX_TEST(non_trivially_relocatable_struct::count.load() == 0); - HPX_TEST(non_trivially_relocatable_struct::move_count.load() == 0); - HPX_TEST(non_trivially_relocatable_struct::dtor_count.load() == 0); - - for (int i = 0; i < N; i++) - { - hpx::construct_at(ptr1 + i, 1234); - } - - // N objects constructed - HPX_TEST(non_trivially_relocatable_struct::count.load() == N); - - // relocate them to ptr2 - uninitialized_relocate(hpx::execution::par, ptr1, ptr1 + N, ptr2); - - sprintf(msg, "count: %d, move_count: %d, dtor_count: %d", - non_trivially_relocatable_struct::count.load(), - non_trivially_relocatable_struct::move_count.load(), - non_trivially_relocatable_struct::dtor_count.load()); - - // All creations - destructions balance out - HPX_TEST_MSG(non_trivially_relocatable_struct::count.load() == N, msg); - - // Every object was moved from and then destroyed - HPX_TEST_MSG( - non_trivially_relocatable_struct::move_count.load() == N, msg); - HPX_TEST_MSG( - non_trivially_relocatable_struct::dtor_count.load() == N, msg); - - for (int i = 0; i < N; i++) - { - HPX_TEST(ptr2[i].data == 1234); - } - - std::destroy_n(ptr2, N); - - std::free(mem1); - std::free(mem2); - } - { - void* mem1 = - std::malloc(N * sizeof(non_trivially_relocatable_struct_throwing)); - void* mem2 = - std::malloc(N * sizeof(non_trivially_relocatable_struct_throwing)); - - HPX_TEST(mem1 && mem2); - - non_trivially_relocatable_struct_throwing* ptr1 = - static_cast(mem1); - non_trivially_relocatable_struct_throwing* ptr2 = - static_cast(mem2); - - HPX_TEST(non_trivially_relocatable_struct_throwing::count.load() == 0); - HPX_TEST( - non_trivially_relocatable_struct_throwing::move_count.load() == 0); - HPX_TEST( - non_trivially_relocatable_struct_throwing::dtor_count.load() == 0); - - for (int i = 0; i < N; i++) - { - hpx::construct_at(ptr1 + i, 1234); - } - - // N objects constructed - HPX_TEST(non_trivially_relocatable_struct_throwing::count.load() == N); - - // relocate them to ptr2 - try - { - uninitialized_relocate(hpx::execution::par, ptr1, ptr1 + N, ptr2); - HPX_UNREACHABLE; // should have thrown - } - catch (...) - { - } - - sprintf(msg, "count: %d, move_count: %d, dtor_count: %d", - non_trivially_relocatable_struct_throwing::count.load(), - non_trivially_relocatable_struct_throwing::move_count.load(), - non_trivially_relocatable_struct_throwing::dtor_count.load()); - - // After catching, all objects must be destroyed - HPX_TEST_MSG( - non_trivially_relocatable_struct_throwing::count == 0, msg); - - std::free(mem1); - std::free(mem2); - } - return hpx::local::finalize(); -} - -int main(int argc, char* argv[]) -{ - hpx::local::init(hpx_main, argc, argv); - return hpx::util::report_errors(); -} diff --git a/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocaten.cpp b/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocaten.cpp index 7814ed4041bb..45e0780de33d 100644 --- a/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocaten.cpp +++ b/libs/core/algorithms/tests/unit/algorithms/uninitialized_relocaten.cpp @@ -4,268 +4,518 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +#include #include #include #include -#define N 50 -#define K 10 +#include +#include +#include +#include + +constexpr int N = 500; // number of objects to construct +constexpr int M = 200; // number of objects to relocate +constexpr int K = 100; // number of objects to relocate before throwing + +static_assert(N > M); +static_assert(M > K); using hpx::experimental::is_trivially_relocatable_v; using hpx::experimental::uninitialized_relocate_n; -struct trivially_relocatable_struct +std::mutex m; + +template +void simple_mutex_operation(F&& f) +{ + std::lock_guard lk(m); + f(); +} + +// enum for the different types of objects +enum relocation_category { - static int count; - static int move_count; - static int dtor_count; + trivially_relocatable, + non_trivially_relocatable, + non_trivially_relocatable_throwing +}; + +template +struct counted_struct +{ + static std::set*> made; + static std::atomic moved; + static std::atomic destroyed; int data; - explicit trivially_relocatable_struct(int data) + explicit counted_struct(int data) : data(data) { - count++; + // Check that we are not constructing an object on top of another + simple_mutex_operation([&]() { + HPX_TEST(!made.count(this)); + made.insert(this); + }); } - trivially_relocatable_struct(trivially_relocatable_struct&& other) + + counted_struct(counted_struct&& other) noexcept( + c != non_trivially_relocatable_throwing) : data(other.data) { - move_count++; - count++; + if constexpr (c == non_trivially_relocatable_throwing) + { + if (moved++ == K - 1) + { + throw 42; + } + } + else + { + moved++; + } + + // Check that we are not constructing an object on top of another + simple_mutex_operation([&]() { + // Unless we are testing overlapping relocation + // we should not be move-constructing an object on top of another + if constexpr (!overlapping_test) + { + HPX_TEST(!made.count(this)); + } + made.insert(this); + }); } - ~trivially_relocatable_struct() + + ~counted_struct() { - dtor_count++; - count--; + destroyed++; + + // Check that the object was constructed + // and not already destroyed + simple_mutex_operation([&]() { + HPX_TEST(made.count(this)); + made.erase(this); + }); } // making sure the address is never directly accessed - friend void operator&(trivially_relocatable_struct) = delete; + friend void operator&(counted_struct) = delete; }; -int trivially_relocatable_struct::count = 0; -int trivially_relocatable_struct::move_count = 0; -int trivially_relocatable_struct::dtor_count = 0; +template +std::set*> + counted_struct::made; + +template +std::atomic counted_struct::moved = 0; + +template +std::atomic counted_struct::destroyed = 0; + +// Non overlapping relocation testing mechanisms +using trivially_relocatable_struct = counted_struct; HPX_DECLARE_TRIVIALLY_RELOCATABLE(trivially_relocatable_struct); -static_assert(is_trivially_relocatable_v); -struct non_trivially_relocatable_struct -{ - static int count; - static int move_count; - static int dtor_count; - int data; +using non_trivially_relocatable_struct = + counted_struct; - explicit non_trivially_relocatable_struct(int data) - : data(data) - { - count++; - } - // mark as noexcept to enter simpler relocation path - non_trivially_relocatable_struct( - non_trivially_relocatable_struct&& other) noexcept - : data(other.data) - { - move_count++; - count++; - } - ~non_trivially_relocatable_struct() - { - dtor_count++; - count--; - } +using non_trivially_relocatable_struct_throwing = + counted_struct; - // making sure the address is never directly accessed - friend void operator&(non_trivially_relocatable_struct) = delete; -}; -int non_trivially_relocatable_struct::count = 0; -int non_trivially_relocatable_struct::move_count = 0; -int non_trivially_relocatable_struct::dtor_count = 0; +// Overlapping relocation testing mechanisms +using trivially_relocatable_struct_overlapping = + counted_struct; +HPX_DECLARE_TRIVIALLY_RELOCATABLE(trivially_relocatable_struct_overlapping); + +using non_trivially_relocatable_struct_overlapping = + counted_struct; + +using non_trivially_relocatable_struct_throwing_overlapping = + counted_struct; + +// Check that the correct types are trivially relocatable +static_assert(is_trivially_relocatable_v); +static_assert( + is_trivially_relocatable_v); static_assert(!is_trivially_relocatable_v); +static_assert( + !is_trivially_relocatable_v); -struct non_trivially_relocatable_struct_throwing +static_assert( + !is_trivially_relocatable_v); +static_assert(!is_trivially_relocatable_v< + non_trivially_relocatable_struct_throwing_overlapping>); + +void clear() { - static int count; - static int move_count; - static int dtor_count; + // Reset for the next test + trivially_relocatable_struct::moved = 0; + trivially_relocatable_struct::destroyed = 0; + trivially_relocatable_struct::made.clear(); + + trivially_relocatable_struct_overlapping::moved = 0; + trivially_relocatable_struct_overlapping::destroyed = 0; + trivially_relocatable_struct_overlapping::made.clear(); + + non_trivially_relocatable_struct::moved = 0; + non_trivially_relocatable_struct::destroyed = 0; + non_trivially_relocatable_struct::made.clear(); + + non_trivially_relocatable_struct_overlapping::moved = 0; + non_trivially_relocatable_struct_overlapping::destroyed = 0; + non_trivially_relocatable_struct_overlapping::made.clear(); + + non_trivially_relocatable_struct_throwing::moved = 0; + non_trivially_relocatable_struct_throwing::destroyed = 0; + non_trivially_relocatable_struct_throwing::made.clear(); + + non_trivially_relocatable_struct_throwing_overlapping::moved = 0; + non_trivially_relocatable_struct_throwing_overlapping::destroyed = 0; + non_trivially_relocatable_struct_throwing_overlapping::made.clear(); +} - int data; +template +std::pair setup() +{ + clear(); - explicit non_trivially_relocatable_struct_throwing(int data) - : data(data) - { - count++; - } - // do not mark as noexcept to enter try-catch relocation path - non_trivially_relocatable_struct_throwing( - non_trivially_relocatable_struct_throwing&& other) - : data(other.data) - { - if (move_count == K) - { - throw 42; - } - move_count++; + void* mem1 = std::malloc(N * sizeof(T)); + void* mem2 = std::malloc(N * sizeof(T)); - count++; - } - ~non_trivially_relocatable_struct_throwing() + HPX_TEST(mem1 && mem2); + + T* ptr1 = static_cast(mem1); + T* ptr2 = static_cast(mem2); + + HPX_TEST(T::made.size() == 0); + HPX_TEST(T::moved == 0); + HPX_TEST(T::destroyed == 0); + + for (int i = 0; i < N; i++) { - dtor_count++; - count--; + hpx::construct_at(ptr1 + i, i); } - // making sure the address is never directly accessed - friend void operator&(non_trivially_relocatable_struct_throwing) = delete; -}; + // fill ptr2 with 0 after M + std::fill(static_cast(mem2) + M * sizeof(T), + static_cast(mem2) + N * sizeof(T), std::byte{0}); -int non_trivially_relocatable_struct_throwing::count = 0; -int non_trivially_relocatable_struct_throwing::move_count = 0; -int non_trivially_relocatable_struct_throwing::dtor_count = 0; + // N objects constructed + HPX_TEST(T::made.size() == N); -static_assert( - !is_trivially_relocatable_v); + return {ptr1, ptr2}; +} -int hpx_main() +template +void test() { - { - void* mem1 = std::malloc(N * sizeof(trivially_relocatable_struct)); - void* mem2 = std::malloc(N * sizeof(trivially_relocatable_struct)); + { // Non-overlapping trivially relocatable + auto [ptr1, ptr2] = setup(); + + for (int i = 0; i < N; i++) + { + // Check that the objects were constructed in the first place + HPX_TEST(trivially_relocatable_struct::made.count(ptr1 + i)); + } - HPX_TEST(mem1 && mem2); + // relocate M objects to ptr2 + uninitialized_relocate_n(Ex{}, ptr1, M, ptr2); - trivially_relocatable_struct* ptr1 = - static_cast(mem1); - trivially_relocatable_struct* ptr2 = - static_cast(mem2); + // bookkeeping + // Artificially add and remove the objects to the set that are where + // relocated because the relocation is trivial + for (int i = 0; i < M; i++) + { + trivially_relocatable_struct::made.erase(ptr1 + i); + trivially_relocatable_struct::made.insert(ptr2 + i); + } - HPX_TEST(trivially_relocatable_struct::count == 0); - HPX_TEST(trivially_relocatable_struct::move_count == 0); - HPX_TEST(trivially_relocatable_struct::dtor_count == 0); + // No move constructor or destructor should be called + HPX_TEST(trivially_relocatable_struct::moved == 0); + HPX_TEST(trivially_relocatable_struct::destroyed == 0); - for (int i = 0; i < N; i++) + // Objects not touched + for (int i = M; i < N; i++) + { + HPX_TEST(ptr1[i].data == i); + } + + // Objects moved from ptr1 to ptr2 + for (int i = 0; i < M; i++) + { + HPX_TEST(ptr2[i].data == i); + } + + // make sure the memory beyond M is untouched + for (std::byte* p = reinterpret_cast(ptr2 + M); + p < reinterpret_cast(ptr2 + N); p++) { - hpx::construct_at(ptr1 + i, 1234); + HPX_TEST(*p == std::byte{0}); } - // N objects constructed - HPX_TEST(trivially_relocatable_struct::count == N); + // From our perspective, the objects in ptr1 are destroyed + std::destroy(ptr1 + M, ptr1 + N); + std::destroy(ptr2, ptr2 + M); + + HPX_TEST(trivially_relocatable_struct::made.empty()); + + std::free(ptr1); + std::free(ptr2); + } + { // Non-overlapping non-trivially relocatable + auto [ptr1, ptr2] = setup(); // relocate them to ptr2 - uninitialized_relocate_n(ptr1, N, ptr2); + uninitialized_relocate_n(Ex{}, ptr1, M, ptr2); - // All creations - destructions balance out - HPX_TEST(trivially_relocatable_struct::count == N); + // M move constructors were called and M destructors + HPX_TEST(non_trivially_relocatable_struct::moved == M); + HPX_TEST(non_trivially_relocatable_struct::destroyed == M); - // No move constructor or destructor should be called - HPX_TEST(trivially_relocatable_struct::move_count == 0); - HPX_TEST(trivially_relocatable_struct::dtor_count == 0); + // Objects not touched + for (int i = M; i < N; i++) + { + HPX_TEST(ptr1[i].data == i); + } - for (int i = 0; i < N; i++) + // Objects moved from ptr1 to ptr2 + for (int i = 0; i < M; i++) { - HPX_TEST(ptr2[i].data == 1234); + HPX_TEST(ptr2[i].data == i); } - std::destroy(ptr2, ptr2 + N); + // make sure the memory beyond M is untouched + for (std::byte* p = reinterpret_cast(ptr2 + M); + p < reinterpret_cast(ptr2 + N); p++) + { + HPX_TEST(*p == std::byte{0}); + } + + std::destroy(ptr1 + M, ptr1 + N); + std::destroy(ptr2, ptr2 + M); + + HPX_TEST(non_trivially_relocatable_struct::made.empty()); - std::free(mem1); - std::free(mem2); + std::free(ptr1); + std::free(ptr2); } - { - void* mem1 = std::malloc(N * sizeof(non_trivially_relocatable_struct)); - void* mem2 = std::malloc(N * sizeof(non_trivially_relocatable_struct)); + { // Non-overlapping non-trivially relocatable throwing + auto [ptr1, ptr2] = setup(); - HPX_TEST(mem1 && mem2); + // relocate M objects to ptr2 + try + { + uninitialized_relocate_n(Ex{}, ptr1, M, ptr2); + HPX_UNREACHABLE; // should have thrown + } + catch (...) + { + } - non_trivially_relocatable_struct* ptr1 = - static_cast(mem1); - non_trivially_relocatable_struct* ptr2 = - static_cast(mem2); + // If the order is sequenced we can guarantee that no moving + // occurs after the exception is thrown + if constexpr (std::is_same_v) + { + // K move constructors were called, and then the last one throws + HPX_TEST(non_trivially_relocatable_struct_throwing::moved == K); + + // K - 1 objects where destroyed after being moved-from + // and then M destructors were called: K on the old range and + // M - K on the new range + HPX_TEST(non_trivially_relocatable_struct_throwing::destroyed == + K - 1 + M); + } - HPX_TEST(non_trivially_relocatable_struct::count == 0); - HPX_TEST(non_trivially_relocatable_struct::move_count == 0); - HPX_TEST(non_trivially_relocatable_struct::dtor_count == 0); + // After the exception is caught, all the objects in the old and the new + // range are destroyed, so M objects are destroyed on each range + // In the case of parallel execution, the number of moves done before stopping + // is arbitrary, since the exception in one thread is not guaranteed to stop + // all the other threads. + HPX_TEST( + non_trivially_relocatable_struct_throwing::made.size() == N - M); - for (int i = 0; i < N; i++) + // The objects in the ends of ptr1 are still valid + for (int i = M; i < N; i++) { - hpx::construct_at(ptr1 + i, 1234); + HPX_TEST(ptr1[i].data == i); } - // N objects constructed - HPX_TEST(non_trivially_relocatable_struct::count == N); + // make sure the memory beyond M is untouched + for (std::byte* p = reinterpret_cast(ptr2 + M); + p < reinterpret_cast(ptr2 + N); p++) + { + HPX_TEST(*p == std::byte{0}); + } - // relocate them to ptr2 - uninitialized_relocate_n(ptr1, N, ptr2); + std::destroy(ptr1 + M, ptr1 + N); - // All creations - destructions balance out - HPX_TEST(non_trivially_relocatable_struct::count == N); + HPX_TEST(non_trivially_relocatable_struct_throwing::made.empty()); - // Every object was moved from and then destroyed - HPX_TEST(non_trivially_relocatable_struct::move_count == N); - HPX_TEST(non_trivially_relocatable_struct::dtor_count == N); + std::free(ptr1); + std::free(ptr2); + } + clear(); - for (int i = 0; i < N; i++) + return; +} + +template +void test_overlapping() +{ + // using Ex = hpx::execution::sequenced_policy; + // static_assert(std::is_same_v); + + constexpr int offset = 4; + + // relocating M objects `offset` positions forward + static_assert(M + offset <= N); + + { // Overlapping trivially-relocatable + auto [ptr, ___] = setup(); + + // Destroy the objects that will be overwritten for bookkeeping + std::destroy(ptr, ptr + offset); + HPX_TEST(trivially_relocatable_struct_overlapping::destroyed == offset); + + // relocate M objects `offset` positions backwards + uninitialized_relocate_n(Ex{}, ptr + offset, M, ptr); + + // Artificially remove the objects from the set for bookkeeping + for (int i = offset; i < M + offset; i++) + { + trivially_relocatable_struct_overlapping::made.erase(ptr + i); + } // and add the objects that were relocated + for (int i = 0; i < M; i++) + { + trivially_relocatable_struct_overlapping::made.insert(ptr + i); + } + + // No move constructor or destructor should be called + // because the objects are trivially relocatable + HPX_TEST(trivially_relocatable_struct_overlapping::moved == 0); + HPX_TEST(trivially_relocatable_struct_overlapping::destroyed == offset); + + // Objects relocated backwards + for (int i = 0; i < M; i++) { - HPX_TEST(ptr2[i].data == 1234); + HPX_TEST(ptr[i].data == i + offset); } - std::destroy(ptr2, ptr2 + N); + // Objects not touched + for (int i = M + offset; i < N; i++) + { + HPX_TEST(ptr[i].data == i); + } - std::free(mem1); - std::free(mem2); + // Destroy objects within their lifetime + // from our perspective objects in the range [M, M + offset) are destroyed + std::destroy(ptr, ptr + M); + std::destroy(ptr + M + offset, ptr + N); + + HPX_TEST(trivially_relocatable_struct_overlapping::made.empty()); + + std::free(ptr); } - { - void* mem1 = - std::malloc(N * sizeof(non_trivially_relocatable_struct_throwing)); - void* mem2 = - std::malloc(N * sizeof(non_trivially_relocatable_struct_throwing)); + { // Overlapping non-trivially relocatable + auto [ptr, ___] = setup(); - HPX_TEST(mem1 && mem2); + // Destroy the objects that will be overwritten for bookkeeping purposes + std::destroy(ptr, ptr + offset); + HPX_TEST( + non_trivially_relocatable_struct_overlapping::destroyed == offset); - non_trivially_relocatable_struct_throwing* ptr1 = - static_cast(mem1); - non_trivially_relocatable_struct_throwing* ptr2 = - static_cast(mem2); + // relocate backwards them to ptr2 + uninitialized_relocate_n(Ex{}, ptr + offset, M, ptr); - HPX_TEST(non_trivially_relocatable_struct_throwing::count == 0); - HPX_TEST(non_trivially_relocatable_struct_throwing::move_count == 0); - HPX_TEST(non_trivially_relocatable_struct_throwing::dtor_count == 0); + // M move constructors were called and M destructors + prior destructors + HPX_TEST(non_trivially_relocatable_struct_overlapping::moved == M); + HPX_TEST(non_trivially_relocatable_struct_overlapping::destroyed == + M + offset); - for (int i = 0; i < N; i++) + // Objects relocated forwards + for (int i = 0; i < M; i++) { - hpx::construct_at(ptr1 + i, 1234); + HPX_TEST(ptr[i].data == i + offset); } - // N objects constructed - HPX_TEST(non_trivially_relocatable_struct_throwing::count == N); + // Objects not touched + for (int i = M + offset; i < N; i++) + { + HPX_TEST(ptr[i].data == i); + } - // relocate them to ptr2 + // Destroy objects within their lifetime + // objects in the range [M, M + offset) are destroyed + std::destroy(ptr, ptr + M); + std::destroy(ptr + M + offset, ptr + N); + + HPX_TEST(non_trivially_relocatable_struct_overlapping::made.empty()); + + std::free(ptr); + } + { // Overlapping non-trivially relocatable throwing + auto [ptr, ___] = + setup(); + + // Destroy the objects that will be overwritten for bookkeeping purposes + std::destroy(ptr, ptr + offset); + HPX_TEST( + non_trivially_relocatable_struct_throwing_overlapping::destroyed == + offset); + + // relocate them backwards try { - uninitialized_relocate_n(ptr1, N, ptr2); + uninitialized_relocate_n(Ex{}, ptr + offset, M, ptr); HPX_UNREACHABLE; // should have thrown } catch (...) { } - // K move constructors were called - HPX_TEST(non_trivially_relocatable_struct_throwing::move_count == K); + // Because we know the execution is sequenced: - // K - 1 destructors were called to balance out the move constructors - // (- 1 because the last move constructor throws) - // and then N + 1 destructors were called: K on the old range and - // N - (K - 1) = N - K + 1 on the new range + // K move constructors were called, and then the last one throws HPX_TEST( - non_trivially_relocatable_struct_throwing::dtor_count == N + K); + non_trivially_relocatable_struct_throwing_overlapping::moved == K); + + // K - 1 objects where destroyed after being moved-from + // and then M destructors were called: K on the new range and + // M - K on the old range + // + offset from prior the relocation + HPX_TEST( + non_trivially_relocatable_struct_throwing_overlapping::destroyed == + K - 1 + M + offset); + + // The objects in the end of ptr1 are still valid + for (int i = M + offset; i < N; i++) + { + HPX_TEST(ptr[i].data == i); + } + + // Destroy objects within their lifetime + std::destroy(ptr + M + offset, ptr + N); - // It stops at K, so K-1 move-destruct pairs have been executed - // after this N - (K - 1) destructs will be done on the old range - // and K - 1 on the new range. giving 2*N total destructs + HPX_TEST(non_trivially_relocatable_struct_throwing_overlapping::made + .empty()); - std::free(mem1); - std::free(mem2); + std::free(ptr); } + clear(); + + return; +} + +int hpx_main() +{ + test(); + test(); + + test_overlapping(); + test_overlapping(); + return hpx::local::finalize(); } diff --git a/libs/core/type_support/CMakeLists.txt b/libs/core/type_support/CMakeLists.txt index ba11cca32dd8..d3c9ef8c3e3a 100644 --- a/libs/core/type_support/CMakeLists.txt +++ b/libs/core/type_support/CMakeLists.txt @@ -25,7 +25,7 @@ set(type_support_headers hpx/type_support/meta.hpp hpx/type_support/relocate_at.hpp hpx/type_support/static.hpp - hpx/type_support/uninitialized_relocate_n_primitive.hpp + hpx/type_support/uninitialized_relocation_primitives.hpp hpx/type_support/unwrap_ref.hpp hpx/type_support/unused.hpp hpx/type_support/void_guard.hpp diff --git a/libs/core/type_support/include/hpx/type_support/uninitialized_relocate_n_primitive.hpp b/libs/core/type_support/include/hpx/type_support/uninitialized_relocate_n_primitive.hpp deleted file mode 100644 index ebd4bfb9ef12..000000000000 --- a/libs/core/type_support/include/hpx/type_support/uninitialized_relocate_n_primitive.hpp +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) 2023 Isidoros Tsaousis-Seiras -// -// SPDX-License-Identifier: BSL-1.0 -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) - -#pragma once - -#include -#include -#include -#include - -#include -#include - -#if defined(HPX_HAVE_P1144_STD_RELOCATE_AT) -#include -#endif - -namespace hpx::experimental::util { - -#if defined(HPX_HAVE_P1144_STD_RELOCATE_AT) - using std::uninitialized_relocate; -#else - - namespace detail { - struct buffer_memcpy_tag - { - }; - - struct for_loop_nothrow_tag - { - }; - - struct for_loop_try_catch_tag - { - }; - - template - struct relocation_traits - { - using in_type = typename std::iterator_traits::value_type; - using out_type = typename std::iterator_traits::value_type; - - constexpr static bool valid_relocation = - is_relocatable_from_v; - // ^^^^ Cannot relocate between unrelated types - - constexpr static bool is_memcpyable = - is_trivially_relocatable_v && - // ^^^^ The important check - !std::is_volatile_v && !std::is_volatile_v; - // ^^^^ Cannot memcpy volatile types - - constexpr static bool is_buffer_memcpyable = - is_memcpyable && iterators_are_contiguous_v; - - constexpr static bool can_move_construct_nothrow = - std::is_nothrow_constructible_v; - // This is to skip the try-catch block - // type_dst is treated as an rvalue by is_nothrow_constructible - - constexpr static bool is_noexcept_relocatable_v = - can_move_construct_nothrow || is_memcpyable; - // If memcpy is not possible, we need to check if the move - // constructor is noexcept - - // Using a tag to distinguish implementations - // clang-format off - using implementation_tag = std::conditional_t< - is_buffer_memcpyable, - buffer_memcpy_tag, - std::conditional_t - >; - // clang-format on - }; - - template - FwdIter uninitialized_relocate_n_primitive_helper( - InIter first, Size n, FwdIter dst, buffer_memcpy_tag) noexcept - { - if (n != 0) - { - std::byte const* first_byte = - reinterpret_cast(std::addressof(*first)); - - std::byte* dst_byte = const_cast( - reinterpret_cast(std::addressof(*dst))); - - Size n_bytes = n * sizeof(*first); - - // Ideally we would want to convey to the compiler - // That the new buffer actually contains objects - // within their lifetime. But this is not possible - // with current language features. - std::memmove(dst_byte, first_byte, n_bytes); - - dst += n; - } - - return dst; - } - - template - // Either the buffer is not contiguous or the types are no-throw - // move constructible but not trivially relocatable - FwdIter uninitialized_relocate_n_primitive_helper( - InIter first, Size n, FwdIter dst, for_loop_nothrow_tag) noexcept - { - for (Size i = 0; i < n; ++first, ++dst, ++i) - { - // if the type is trivially relocatable this will be a memcpy - // otherwise it will be a move + destroy - hpx::experimental::detail::relocate_at_helper( - std::addressof(*first), std::addressof(*dst)); - } - - return dst; - } - - template - FwdIter uninitialized_relocate_n_primitive_helper( - InIter first, Size n, FwdIter dst, for_loop_try_catch_tag) - { - FwdIter original_dst = dst; - - for (Size i = 0; i < n; ++first, ++dst, ++i) - { - try - { - // the move + destroy version will be used - hpx::experimental::detail::relocate_at_helper( - std::addressof(*first), std::addressof(*dst)); - } - catch (...) - { - // destroy all objects other that the one - // that caused the exception - // (relocate_at already destroyed that one) - - // destroy all objects constructed so far - std::destroy(original_dst, dst); - // destroy all the objects not relocated yet - std::destroy_n(++first, n - i - 1); - // Note: - // using destroy_n instead of destroy + advance - // to avoid calculating the distance - - throw; - } - } - - return dst; - } - - } // namespace detail - - template - // clang-format off - FwdIter uninitialized_relocate_n_primitive(InIter first, Size n, - FwdIter dst, iterators_are_contiguous_t) noexcept( - detail::relocation_traits::is_noexcept_relocatable_v) - // clang-format on - { - static_assert( - detail::relocation_traits::valid_relocation, - "uninitialized_move(first, last, dst) must be well-formed"); - - using implementation_tag = typename detail::relocation_traits::implementation_tag; - - return detail::uninitialized_relocate_n_primitive_helper( - first, n, dst, implementation_tag{}); - } - - template - FwdIter uninitialized_relocate_n_primitive(InIter first, Size n, - FwdIter dst) noexcept(detail::relocation_traits::is_noexcept_relocatable_v) - { - using iterators_are_contiguous_default_t = - std::bool_constant && - hpx::traits::is_contiguous_iterator_v>; - - return uninitialized_relocate_n_primitive( - first, n, dst, iterators_are_contiguous_default_t{}); - } - -#endif // defined(HPX_HAVE_P1144_STD_RELOCATE_AT) - -} // namespace hpx::experimental::util diff --git a/libs/core/type_support/include/hpx/type_support/uninitialized_relocation_primitives.hpp b/libs/core/type_support/include/hpx/type_support/uninitialized_relocation_primitives.hpp new file mode 100644 index 000000000000..b5006f6e51dc --- /dev/null +++ b/libs/core/type_support/include/hpx/type_support/uninitialized_relocation_primitives.hpp @@ -0,0 +1,497 @@ +// Copyright (c) 2023 Isidoros Tsaousis-Seiras +// +// SPDX-License-Identifier: BSL-1.0 +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +#pragma once + +#include +#include +#include +#include + +#include // for memmove +#include + +#include + +#if defined(HPX_HAVE_P1144_RELOCATE_AT) +#include +#endif + +namespace hpx::experimental::util { + + namespace detail { // Utility metafunctions + + struct buffer_memcpy_tag + { + }; + + struct for_loop_nothrow_tag + { + }; + + struct for_loop_try_catch_tag + { + }; + + template + struct relocation_traits + { + using in_type = typename std::iterator_traits::value_type; + using out_type = typename std::iterator_traits::value_type; + + constexpr static bool valid_relocation = + is_relocatable_from_v; + // ^^^^ Cannot relocate between unrelated types + + constexpr static bool is_memcpyable = + is_trivially_relocatable_v && + // ^^^^ The important check + !std::is_volatile_v && !std::is_volatile_v; + // ^^^^ Cannot memcpy volatile types + + constexpr static bool is_buffer_memcpyable = + is_memcpyable && iterators_are_contiguous_v; + + constexpr static bool can_move_construct_nothrow = + std::is_nothrow_constructible_v; + // This is to skip the try-catch block + // type_dst is treated as an rvalue by is_nothrow_constructible + + constexpr static bool is_noexcept_relocatable_v = + can_move_construct_nothrow || is_memcpyable; + // If memcpy is not possible, we need to check if the move + // constructor is noexcept + + // Using a tag to distinguish implementations + // clang-format off + using implementation_tag = std::conditional_t< + is_buffer_memcpyable, + buffer_memcpy_tag, + std::conditional_t + >; + // clang-format on + }; + } // namespace detail + +#if defined(HPX_HAVE_P1144_RELOCATE_AT) + ////////////////////////////// + // uninitialized_relocate_n // + ////////////////////////////// + template // Dummy is used retain the same signature + // as the implementation before P1144 + // clang-format off + std::tuple uninitialized_relocate_n_primitive(InIter first, Size n, + FwdIter dst, Dummy) noexcept( + detail::relocation_traits::is_noexcept_relocatable_v) + // clang-format on + { + static_assert( + detail::relocation_traits::valid_relocation, + "uninitialized_move(first, last, dst) must be well-formed"); + + return std::uninitialized_relocate_n(first, n, dst); + } + + template + std::tuple uninitialized_relocate_n_primitive(InIter first, + Size n, FwdIter dst) noexcept(detail::relocation_traits::is_noexcept_relocatable_v) + { + return uninitialized_relocate_n_primitive(first, n, dst, bool{}); + } + + //////////////////////////// + // uninitialized_relocate // + //////////////////////////// + template // Dummy is used retain the same signature + // as the implementation before P1144 + // clang-format off + std::tuple uninitialized_relocate_primitive(InIter first, Sent last, + FwdIter dst, Dummy) noexcept( + detail::relocation_traits::is_noexcept_relocatable_v) + // clang-format on + { + // TODO CHECK SENT + static_assert( + detail::relocation_traits::valid_relocation, + "uninitialized_move(first, last, dst) must be well-formed"); + + return std::uninitialized_relocate(first, last, dst); + } + + template + std::tuple uninitialized_relocate_primitive(InIter first, + Sent last, FwdIter dst) noexcept(detail::relocation_traits::is_noexcept_relocatable_v) + { + return uninitialized_relocate_primitive(first, last, dst, bool{}); + } + + ///////////////////////////////////// + // uninitialized_relocate_backward // + ///////////////////////////////////// + template // Dummy is used retain the same signature + // as the implementation before P1144 + // clang-format off + std::tuple uninitialized_relocate_backward_primitive( + BiIter1 first, BiIter1 last,BiIter2 dst_last, Dummy) noexcept( + detail::relocation_traits::is_noexcept_relocatable_v) + // clang-format on + { + // TODO CHECK SENT + static_assert( + detail::relocation_traits::valid_relocation, + "uninitialized_move(first, last, dst) must be well-formed"); + + return std::uninitialized_relocate_backward(first, last, dst_last); + } + + template + std::tuple uninitialized_relocate_backward_primitive( + BiIter1 first, BiIter1 last, + BiIter2 dst_last) noexcept(detail::relocation_traits::is_noexcept_relocatable_v) + { + return uninitialized_relocate_backward_primitive( + first, last, dst_last, bool{}); + } +#else + + namespace detail { + ////////////////////////////// + // uninitialized_relocate_n // + ////////////////////////////// + template + std::tuple uninitialized_relocate_n_primitive_helper( + InIter first, Size n, FwdIter dst, buffer_memcpy_tag) noexcept + { + if (n != 0) + { + std::byte const* first_byte = + reinterpret_cast(std::addressof(*first)); + + std::byte* dst_byte = const_cast( + reinterpret_cast(std::addressof(*dst))); + + Size n_bytes = n * sizeof(*first); + + // Ideally we would want to convey to the compiler + // That the new buffer actually contains objects + // within their lifetime. But this is not possible + // with current language features. + std::memmove(dst_byte, first_byte, n_bytes); + + dst += n; + } + + return {first, dst}; + } + + template + // Either the buffer is not contiguous or the types are no-throw + // move constructible but not trivially relocatable + std::tuple uninitialized_relocate_n_primitive_helper( + InIter first, Size n, FwdIter dst, for_loop_nothrow_tag) noexcept + { + for (Size i = 0; i < n; ++first, ++dst, ++i) + { + // if the type is trivially relocatable this will be a memcpy + // otherwise it will be a move + destroy + hpx::experimental::detail::relocate_at_helper( + std::addressof(*first), std::addressof(*dst)); + } + + return {first, dst}; + } + + template + std::tuple uninitialized_relocate_n_primitive_helper( + InIter first, Size n, FwdIter dst, for_loop_try_catch_tag) + { + FwdIter original_dst = dst; + + for (Size i = 0; i < n; ++first, ++dst, ++i) + { + try + { + // the move + destroy version will be used + hpx::experimental::detail::relocate_at_helper( + std::addressof(*first), std::addressof(*dst)); + } + catch (...) + { + // destroy all objects other that the one + // that caused the exception + // (relocate_at already destroyed that one) + + // destroy all objects constructed so far + std::destroy(original_dst, dst); + // destroy all the objects not relocated yet + std::destroy_n(++first, n - i - 1); + // Note: + // using destroy_n instead of destroy + advance + // to avoid calculating the distance + + throw; + } + } + + return {first, dst}; + } + + //////////////////////////// + // uninitialized_relocate // + //////////////////////////// + template + std::tuple uninitialized_relocate_primitive_helper( + InIter first, Sent last, FwdIter dst, buffer_memcpy_tag) noexcept + { + return uninitialized_relocate_n_primitive_helper( + first, std::distance(first, last), dst, buffer_memcpy_tag{}); + } + + template + // Either the buffer is not contiguous or the types are no-throw + // move constructible but not trivially relocatable + std::tuple uninitialized_relocate_primitive_helper( + InIter first, Sent last, FwdIter dst, for_loop_nothrow_tag) noexcept + { + for (; first != last; ++first, ++dst) + { + // if the type is trivially relocatable this will be a memcpy + // otherwise it will be a move + destroy + hpx::experimental::detail::relocate_at_helper( + std::addressof(*first), std::addressof(*dst)); + } + + return {first, dst}; + } + + template + std::tuple uninitialized_relocate_primitive_helper( + InIter first, Sent last, FwdIter dst, for_loop_try_catch_tag) + { + FwdIter original_dst = dst; + + for (; first != last; ++first, ++dst) + { + try + { + // the move + destroy version will be used + hpx::experimental::detail::relocate_at_helper( + std::addressof(*first), std::addressof(*dst)); + } + catch (...) + { + // destroy all objects other that the one + // that caused the exception + // (relocate_at already destroyed that one) + + // destroy all objects constructed so far + std::destroy(original_dst, dst); + // destroy all the objects not relocated yet + std::destroy(++first, last); + + throw; + } + } + + return {first, dst}; + } + + ///////////////////////////////////// + // uninitialized_relocate_backward // + ///////////////////////////////////// + template + std::tuple + uninitialized_relocate_backward_primitive_helper(BiIter1 first, + BiIter1 last, BiIter2 dst_last, buffer_memcpy_tag) noexcept + { + // Here we know the iterators are contiguous + // So calculating the distance and the previous + // iterator is O(1) + auto n_objects = std::distance(first, last); + BiIter2 dst_first = std::prev(dst_last, n_objects); + + return uninitialized_relocate_n_primitive_helper( + first, n_objects, dst_first, buffer_memcpy_tag{}); + } + + template + // Either the buffer is not contiguous or the types are no-throw + // move constructible but not trivially relocatable + // dst_last is one past the last element of the destination + std::tuple + uninitialized_relocate_backward_primitive_helper(BiIter1 first, + BiIter1 last, BiIter2 dst_last, for_loop_nothrow_tag) noexcept + { + while (first != last) + { + // if the type is trivially relocatable this will be a memcpy + // otherwise it will be a move + destroy + std::advance(last, -1); + std::advance(dst_last, -1); + hpx::experimental::detail::relocate_at_helper( + std::addressof(*last), std::addressof(*dst_last)); + } + + return {last, dst_last}; + } + + template + std::tuple + uninitialized_relocate_backward_primitive_helper(BiIter1 first, + BiIter1 last, BiIter2 dst_last, for_loop_try_catch_tag) + { + BiIter2 original_dst_last = dst_last; + + while (first != last) + { + try + { + std::advance(last, -1); + std::advance(dst_last, -1); + // the move + destroy version will be used + hpx::experimental::detail::relocate_at_helper( + std::addressof(*last), std::addressof(*dst_last)); + } + catch (...) + { + // destroy all objects other that the one + // that caused the exception + // (relocate_at already destroyed that one) + + // destroy all objects constructed so far + std::destroy(++dst_last, original_dst_last); + // destroy all the objects not relocated yet + std::destroy(first, last); + + throw; + } + } + + return {last, dst_last}; + } + + } // namespace detail + + ////////////////////////////// + // uninitialized_relocate_n // + ////////////////////////////// + template + // clang-format off + std::tuple uninitialized_relocate_n_primitive(InIter first, Size n, + FwdIter dst, iterators_are_contiguous_t) noexcept( + detail::relocation_traits::is_noexcept_relocatable_v) + // clang-format on + { + static_assert( + detail::relocation_traits::valid_relocation, + "uninitialized_move(first, last, dst) must be well-formed"); + + using implementation_tag = typename detail::relocation_traits::implementation_tag; + + return detail::uninitialized_relocate_n_primitive_helper( + first, n, dst, implementation_tag{}); + } + + template + std::tuple uninitialized_relocate_n_primitive(InIter first, + Size n, FwdIter dst) noexcept(detail::relocation_traits::is_noexcept_relocatable_v) + { + using iterators_are_contiguous_default_t = + std::bool_constant && + hpx::traits::is_contiguous_iterator_v>; + + return uninitialized_relocate_n_primitive( + first, n, dst, iterators_are_contiguous_default_t{}); + } + + //////////////////////////// + // uninitialized_relocate // + //////////////////////////// + template + // clang-format off + std::tuple uninitialized_relocate_primitive(InIter first, Sent last, + FwdIter dst, iterators_are_contiguous_t) noexcept( + detail::relocation_traits::is_noexcept_relocatable_v) + // clang-format on + { + // TODO CHECK SENT + static_assert( + detail::relocation_traits::valid_relocation, + "uninitialized_move(first, last, dst) must be well-formed"); + + using implementation_tag = typename detail::relocation_traits::implementation_tag; + + return detail::uninitialized_relocate_primitive_helper( + first, last, dst, implementation_tag{}); + } + + template + std::tuple uninitialized_relocate_primitive(InIter first, + Sent last, FwdIter dst) noexcept(detail::relocation_traits::is_noexcept_relocatable_v) + { + using iterators_are_contiguous_default_t = + std::bool_constant && + hpx::traits::is_contiguous_iterator_v>; + + return uninitialized_relocate_primitive( + first, last, dst, iterators_are_contiguous_default_t{}); + } + + ///////////////////////////////////// + // uninitialized_relocate_backward // + ///////////////////////////////////// + template + std::tuple uninitialized_relocate_backward_primitive( + BiIter1 first, BiIter1 last, BiIter2 dst_last, + iterators_are_contiguous_t) noexcept(detail::relocation_traits::is_noexcept_relocatable_v) + { + // TODO CHECK SENT + static_assert( + detail::relocation_traits::valid_relocation, + "uninitialized_move(first, last, dst) must be well-formed"); + + using implementation_tag = typename detail::relocation_traits::implementation_tag; + + return detail::uninitialized_relocate_backward_primitive_helper( + first, last, dst_last, implementation_tag{}); + } + + template + std::tuple uninitialized_relocate_backward_primitive( + BiIter1 first, BiIter1 last, + BiIter2 dst_last) noexcept(detail::relocation_traits::is_noexcept_relocatable_v) + { + using iterators_are_contiguous_default_t = + std::bool_constant && + hpx::traits::is_contiguous_iterator_v>; + + return uninitialized_relocate_backward_primitive( + first, last, dst_last, iterators_are_contiguous_default_t{}); + } + +#endif // defined(__cpp_lib_trivially_relocatable) + +} // namespace hpx::experimental::util diff --git a/libs/core/type_support/tests/unit/fail_uninitialized_relocate.cpp b/libs/core/type_support/tests/unit/fail_uninitialized_relocate.cpp index 9e72b1bfc121..1b70c3ee5240 100644 --- a/libs/core/type_support/tests/unit/fail_uninitialized_relocate.cpp +++ b/libs/core/type_support/tests/unit/fail_uninitialized_relocate.cpp @@ -7,7 +7,7 @@ // This test should fail to compile #include -#include +#include using hpx::experimental::util::uninitialized_relocate_n_primitive; diff --git a/libs/core/type_support/tests/unit/uninitialized_relocate_n_primitive.cpp b/libs/core/type_support/tests/unit/uninitialized_relocate_n_primitive.cpp index 854d7d564308..a9db7314fe90 100644 --- a/libs/core/type_support/tests/unit/uninitialized_relocate_n_primitive.cpp +++ b/libs/core/type_support/tests/unit/uninitialized_relocate_n_primitive.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #define N 50 #define K 10