diff --git a/common/common-tests.cmake b/common/common-tests.cmake index df450ea94d..5fd0b42b23 100644 --- a/common/common-tests.cmake +++ b/common/common-tests.cmake @@ -17,7 +17,8 @@ prepend(COMMON_TESTS_SOURCES ${COMMON_DIR}/ type_traits/list_of_types_test.cpp wrappers/span-test.cpp wrappers/string_view-test.cpp - ucontext/ucontext-portable-test.cpp) + ucontext/ucontext-portable-test.cpp + containers/intrusive-list-test.cpp) allow_deprecated_declarations(${COMMON_TESTS_SOURCES}/algorithms/projections-test.cpp) diff --git a/common/containers/intrusive-list-test.cpp b/common/containers/intrusive-list-test.cpp new file mode 100644 index 0000000000..6496a55539 --- /dev/null +++ b/common/containers/intrusive-list-test.cpp @@ -0,0 +1,1033 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "common/containers/intrusive-list.h" + +namespace { + +template +auto values(const List& l) -> std::vector { + std::vector v; + for (const auto& x : l) { + v.push_back(x); + } + + return v; +} + +template +auto reversed_values(List& l) -> std::vector { + std::vector v; + for (auto it = l.rbegin(); it != l.rend(); ++it) { + v.push_back(*it); + } + + return v; +} + +} // namespace + +TEST(intrusive_list_node_base, default_is_not_linked) { + vk::intrusive::details::list_node_base b; + + ASSERT_FALSE(b.is_linked()); +} + +TEST(intrusive_list_node_base, unlink_on_unlinked_is_safe) { + vk::intrusive::details::list_node_base b; + + b.unlink(); + b.unlink(); + + ASSERT_FALSE(b.is_linked()); +} + +TEST(intrusive_list_basic, empty_list) { + vk::intrusive::list> l; + + ASSERT_TRUE(l.empty()); + ASSERT_EQ(l.size(), 0); + ASSERT_EQ(l.begin(), l.end()); + ASSERT_EQ(l.cbegin(), l.cend()); + ASSERT_EQ(l.rbegin(), l.rend()); + ASSERT_EQ(l.crbegin(), l.crend()); +} + +TEST(intrusive_list_basic, push_back_single) { + vk::intrusive::list> l; + vk::intrusive::list_node n{42}; + + l.push_back(n); + + ASSERT_FALSE(l.empty()); + ASSERT_EQ(l.size(), 1); + ASSERT_EQ(l.front(), 42); + ASSERT_EQ(l.back(), 42); + ASSERT_EQ(std::addressof(l.front()), std::addressof(l.back())); +} + +TEST(intrusive_list_basic, push_back_keeps_order) { + vk::intrusive::list> l; + std::array, 3> ns{vk::intrusive::list_node{1}, vk::intrusive::list_node{2}, vk::intrusive::list_node{3}}; + + for (auto& n : ns) { + l.push_back(n); + } + + ASSERT_EQ(l.size(), 3); + ASSERT_EQ(values(l), (std::vector{1, 2, 3})); +} + +TEST(intrusive_list_basic, push_front_single) { + vk::intrusive::list> l; + vk::intrusive::list_node n{42}; + + l.push_front(n); + + ASSERT_FALSE(l.empty()); + ASSERT_EQ(l.size(), 1); + ASSERT_EQ(l.front(), 42); + ASSERT_EQ(l.back(), 42); + ASSERT_EQ(std::addressof(l.front()), std::addressof(l.back())); +} + +TEST(intrusive_list_basic, push_front_reverses_order) { + vk::intrusive::list> l; + std::array, 3> ns{vk::intrusive::list_node{1}, vk::intrusive::list_node{2}, vk::intrusive::list_node{3}}; + + for (auto& n : ns) { + l.push_front(n); + } + + ASSERT_EQ(l.size(), 3); + ASSERT_EQ(values(l), (std::vector{3, 2, 1})); +} + +TEST(intrusive_list_basic, mixed_push_front_and_back) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}, c{3}, d{4}; + + l.push_back(b); // 2 + l.push_front(a); // 1 2 + l.push_back(c); // 1 2 3 + l.push_front(d); // 4 1 2 3 + + ASSERT_EQ(values(l), (std::vector{4, 1, 2, 3})); +} + +TEST(intrusive_list_basic, front_back_are_mutable_references) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.push_back(a); + l.push_back(b); + l.front() = 10; + l.back() = 20; + + ASSERT_EQ(values(l), (std::vector{10, 20})); + ASSERT_EQ(a.value(), 10); + ASSERT_EQ(b.value(), 20); +} + +TEST(intrusive_list_iteration, forward) { + vk::intrusive::list> l; + std::array, 4> ns{vk::intrusive::list_node{1}, vk::intrusive::list_node{2}, vk::intrusive::list_node{3}, + vk::intrusive::list_node{4}}; + + for (auto& n : ns) { + l.push_back(n); + } + + ASSERT_EQ(values(l), (std::vector{1, 2, 3, 4})); +} + +TEST(intrusive_list_iteration, reverse) { + vk::intrusive::list> l; + std::array, 4> ns{vk::intrusive::list_node{1}, vk::intrusive::list_node{2}, vk::intrusive::list_node{3}, + vk::intrusive::list_node{4}}; + + for (auto& n : ns) { + l.push_back(n); + } + + ASSERT_EQ(reversed_values(l), (std::vector{4, 3, 2, 1})); +} + +TEST(intrusive_list_iteration, bidirectional_increment_decrement) { + vk::intrusive::list> l; + std::array, 3> ns{vk::intrusive::list_node{1}, vk::intrusive::list_node{2}, vk::intrusive::list_node{3}}; + + for (auto& n : ns) { + l.push_back(n); + } + auto it = l.begin(); + + ASSERT_EQ(*it, 1); + ASSERT_EQ(*(++it), 2); + ASSERT_EQ(*(it++), 2); + ASSERT_EQ(*it, 3); + ASSERT_EQ(*(--it), 2); + ASSERT_EQ(*(it--), 2); + ASSERT_EQ(*it, 1); + + auto last = l.end(); + --last; + + ASSERT_EQ(*last, 3); +} + +TEST(intrusive_list_iteration, const_iteration) { + vk::intrusive::list> l; + std::array, 3> ns{vk::intrusive::list_node{7}, vk::intrusive::list_node{8}, vk::intrusive::list_node{9}}; + + for (auto& n : ns) { + l.push_back(n); + } + const vk::intrusive::list>& cl = l; + + ASSERT_EQ(values(cl), (std::vector{7, 8, 9})); + + std::vector collected; + for (auto it = cl.cbegin(); it != cl.cend(); ++it) { + collected.push_back(*it); + } + + ASSERT_EQ(collected, (std::vector{7, 8, 9})); + + std::vector reversed_collected; + for (auto it = cl.crbegin(); it != cl.crend(); ++it) { + reversed_collected.push_back(*it); + } + + ASSERT_EQ(reversed_collected, (std::vector{9, 8, 7})); +} + +TEST(intrusive_list_iteration, iterator_converts_to_const_and_compares) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}; + + l.push_back(a); + + vk::intrusive::list>::iterator it = l.begin(); + vk::intrusive::list>::const_iterator cit = it; // implicit non-const -> const conversion + + ASSERT_EQ(it, cit); + ASSERT_EQ(cit, it); + ASSERT_FALSE(it != cit); +} + +TEST(intrusive_list_iteration, works_with_std_algorithms) { + vk::intrusive::list> l; + std::array, 5> ns{vk::intrusive::list_node{1}, vk::intrusive::list_node{2}, vk::intrusive::list_node{3}, + vk::intrusive::list_node{4}, vk::intrusive::list_node{5}}; + + for (auto& n : ns) { + l.push_back(n); + } + + ASSERT_EQ(std::distance(l.begin(), l.end()), 5); + + auto found = std::find(l.begin(), l.end(), 3); + + ASSERT_NE(found, l.end()); + ASSERT_EQ(*found, 3); + + ASSERT_EQ(std::find(l.begin(), l.end(), 42), l.end()); + ASSERT_EQ(std::accumulate(l.begin(), l.end(), 0), 15); +} + +TEST(intrusive_list_insert, at_begin) { + vk::intrusive::list> l; + vk::intrusive::list_node a{2}, b{1}; + + l.push_back(a); + auto it = l.insert(l.begin(), b); + + ASSERT_EQ(*it, 1); + ASSERT_EQ(values(l), (std::vector{1, 2})); +} + +TEST(intrusive_list_insert, at_end) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.insert(l.end(), a); + l.insert(l.end(), b); + + ASSERT_EQ(values(l), (std::vector{1, 2})); +} + +TEST(intrusive_list_insert, in_middle_returns_iterator_to_new_node) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, c{3}, b{2}; + + l.push_back(a); + l.push_back(c); + auto pos = std::next(l.begin()); // points at 3 + auto it = l.insert(pos, b); + + ASSERT_EQ(*it, 2); + ASSERT_EQ(values(l), (std::vector{1, 2, 3})); +} + +TEST(intrusive_list_insert, same_node_at_its_own_position_is_noop) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.push_back(a); + l.push_back(b); + // Inserting node 'a' right before itself must not corrupt the list. + auto it = l.insert(l.begin(), a); + + ASSERT_EQ(*it, 1); + ASSERT_EQ(values(l), (std::vector{1, 2})); +} + +TEST(intrusive_list_insert, relinks_node_moving_it_from_another_list) { + vk::intrusive::list> src; + vk::intrusive::list> dst; + vk::intrusive::list_node a{1}, b{2}, c{3}; + + src.push_back(a); + src.push_back(b); + src.push_back(c); + + // Moving 'b' into dst removes it from src (insert unlinks first). + dst.push_back(b); + ASSERT_EQ(values(src), (std::vector{1, 3})); + ASSERT_EQ(values(dst), (std::vector{2})); +} + +TEST(intrusive_list_insert, moves_node_within_same_list) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}, c{3}; + + l.push_back(a); + l.push_back(b); + l.push_back(c); + // Re-insert front node before end -> moves it to the back. + l.insert(l.end(), a); + + ASSERT_EQ(values(l), (std::vector{2, 3, 1})); + ASSERT_EQ(l.size(), 3); +} + +TEST(intrusive_list_erase, single_returns_next) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}, c{3}; + + l.push_back(a); + l.push_back(b); + l.push_back(c); + auto next = l.erase(std::next(l.begin())); // erase 2 + + ASSERT_EQ(*next, 3); + ASSERT_EQ(values(l), (std::vector{1, 3})); +} + +TEST(intrusive_list_erase, front) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.push_back(a); + l.push_back(b); + auto next = l.erase(l.begin()); + + ASSERT_EQ(*next, 2); + ASSERT_EQ(values(l), (std::vector{2})); +} + +TEST(intrusive_list_erase, end) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.push_back(a); + l.push_back(b); + auto next = l.erase(std::prev(l.end())); + + ASSERT_EQ(next, l.end()); + ASSERT_EQ(values(l), (std::vector{1})); +} + +TEST(intrusive_list_erase, range) { + vk::intrusive::list> l; + std::array, 5> ns{vk::intrusive::list_node{1}, vk::intrusive::list_node{2}, vk::intrusive::list_node{3}, + vk::intrusive::list_node{4}, vk::intrusive::list_node{5}}; + + for (auto& n : ns) { + l.push_back(n); + } + // erase [2, 4) -> removes 2 and 3 + auto first = std::next(l.begin()); + auto last = std::next(l.begin(), 3); + auto it = l.erase(first, last); + + ASSERT_EQ(*it, 4); + ASSERT_EQ(values(l), (std::vector{1, 4, 5})); +} + +TEST(intrusive_list_erase, empty_range_is_noop) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.push_back(a); + l.push_back(b); + auto it = l.erase(l.begin(), l.begin()); + + ASSERT_EQ(*it, 1); + ASSERT_EQ(values(l), (std::vector{1, 2})); +} + +TEST(intrusive_list_erase, all_makes_empty) { + vk::intrusive::list> l; + std::array, 3> ns{vk::intrusive::list_node{1}, vk::intrusive::list_node{2}, vk::intrusive::list_node{3}}; + + for (auto& n : ns) { + l.push_back(n); + } + auto it = l.erase(l.begin(), l.end()); + + ASSERT_EQ(it, l.end()); + ASSERT_TRUE(l.empty()); + ASSERT_EQ(l.size(), 0); +} + +TEST(intrusive_list_erase, node_is_reusable_after_erase) { + vk::intrusive::list> l1; + vk::intrusive::list> l2; + vk::intrusive::list_node a{1}; + + l1.push_back(a); + l1.erase(l1.begin()); + + ASSERT_TRUE(l1.empty()); + + // The same node object can be linked into another list afterwards. + l2.push_back(a); + + ASSERT_EQ(values(l2), (std::vector{1})); +} + +TEST(intrusive_list_pop, pop_back) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}, c{3}; + + l.push_back(a); + l.push_back(b); + l.push_back(c); + l.pop_back(); + + ASSERT_EQ(values(l), (std::vector{1, 2})); + + l.pop_back(); + + ASSERT_EQ(values(l), (std::vector{1})); +} + +TEST(intrusive_list_pop, pop_front) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}, c{3}; + + l.push_back(a); + l.push_back(b); + l.push_back(c); + l.pop_front(); + + ASSERT_EQ(values(l), (std::vector{2, 3})); + l.pop_front(); + + ASSERT_EQ(values(l), (std::vector{3})); +} + +TEST(intrusive_list_pop, pop_until_empty) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.push_back(a); + l.push_back(b); + l.pop_front(); + l.pop_back(); + + ASSERT_TRUE(l.empty()); +} + +TEST(intrusive_list_clear, makes_list_empty) { + vk::intrusive::list> l; + std::array, 3> ns{vk::intrusive::list_node{1}, vk::intrusive::list_node{2}, vk::intrusive::list_node{3}}; + + for (auto& n : ns) { + l.push_back(n); + } + l.clear(); + + ASSERT_TRUE(l.empty()); + ASSERT_EQ(l.size(), 0); + ASSERT_EQ(l.begin(), l.end()); +} + +TEST(intrusive_list_clear, list_is_reusable_after_clear) { + vk::intrusive::list> l; + std::array, 3> ns{vk::intrusive::list_node{1}, vk::intrusive::list_node{2}, vk::intrusive::list_node{3}}; + + for (auto& n : ns) { + l.push_back(n); + } + l.clear(); + vk::intrusive::list_node x{10}, y{20}; + l.push_back(x); + l.push_back(y); + + ASSERT_EQ(values(l), (std::vector{10, 20})); +} + +TEST(intrusive_list_swap, two_non_empty) { + vk::intrusive::list> a; + vk::intrusive::list> b; + vk::intrusive::list_node a1{1}, a2{2}; + vk::intrusive::list_node b1{3}, b2{4}, b3{5}; + + a.push_back(a1); + a.push_back(a2); + b.push_back(b1); + b.push_back(b2); + b.push_back(b3); + + a.swap(b); + + ASSERT_EQ(values(a), (std::vector{3, 4, 5})); + ASSERT_EQ(values(b), (std::vector{1, 2})); +} + +TEST(intrusive_list_swap, empty_with_non_empty) { + vk::intrusive::list> a; + vk::intrusive::list> b; + vk::intrusive::list_node b1{1}, b2{2}; + + b.push_back(b1); + b.push_back(b2); + + a.swap(b); + + ASSERT_EQ(values(a), (std::vector{1, 2})); + ASSERT_TRUE(b.empty()); + + a.swap(b); + + ASSERT_TRUE(a.empty()); + ASSERT_EQ(values(b), (std::vector{1, 2})); +} + +TEST(intrusive_list_swap, both_empty) { + vk::intrusive::list> a; + vk::intrusive::list> b; + + a.swap(b); + + ASSERT_TRUE(a.empty()); + ASSERT_TRUE(b.empty()); +} + +TEST(intrusive_list_swap, free_function) { + vk::intrusive::list> a; + vk::intrusive::list> b; + vk::intrusive::list_node a1{1}; + vk::intrusive::list_node b1{2}, b2{3}; + + a.push_back(a1); + b.push_back(b1); + b.push_back(b2); + + swap(a, b); + + ASSERT_EQ(values(a), (std::vector{2, 3})); + ASSERT_EQ(values(b), (std::vector{1})); +} + +TEST(intrusive_list_swap, swap_with_self) { + vk::intrusive::list> a; + vk::intrusive::list_node a1{1}, a2{2}; + + a.push_back(a1); + a.push_back(a2); + + a.swap(a); + + ASSERT_EQ(values(a), (std::vector{1, 2})); + + swap(a, a); + + ASSERT_EQ(values(a), (std::vector{1, 2})); +} + +TEST(intrusive_list_splice, whole_list_at_end) { + vk::intrusive::list> a; + vk::intrusive::list> b; + vk::intrusive::list_node a1{1}, a2{2}, a3{3}; + vk::intrusive::list_node b1{4}, b2{5}; + + a.push_back(a1); + a.push_back(a2); + a.push_back(a3); + b.push_back(b1); + b.push_back(b2); + + b.splice(b.end(), a); + + ASSERT_EQ(values(b), (std::vector{4, 5, 1, 2, 3})); + ASSERT_TRUE(a.empty()); +} + +TEST(intrusive_list_splice, whole_list_at_begin) { + vk::intrusive::list> a; + vk::intrusive::list> b; + vk::intrusive::list_node a1{1}, a2{2}; + vk::intrusive::list_node b1{3}, b2{4}; + + a.push_back(a1); + a.push_back(a2); + b.push_back(b1); + b.push_back(b2); + + b.splice(b.begin(), a); + + ASSERT_EQ(values(b), (std::vector{1, 2, 3, 4})); + ASSERT_TRUE(a.empty()); +} + +TEST(intrusive_list_splice, whole_list_in_middle) { + vk::intrusive::list> a; + vk::intrusive::list> b; + vk::intrusive::list_node a1{1}, a2{2}; + vk::intrusive::list_node b1{3}, b2{4}; + + a.push_back(a1); + a.push_back(a2); + b.push_back(b1); + b.push_back(b2); + + b.splice(std::next(b.begin()), a); // between 3 and 4 + + ASSERT_EQ(values(b), (std::vector{3, 1, 2, 4})); + ASSERT_TRUE(a.empty()); +} + +TEST(intrusive_list_splice, single_element) { + vk::intrusive::list> a; + vk::intrusive::list> b; + vk::intrusive::list_node a1{1}, a2{2}, a3{3}; + + a.push_back(a1); + a.push_back(a2); + a.push_back(a3); + + b.splice(b.end(), a, std::next(a.begin())); // move `2` + + ASSERT_EQ(values(b), (std::vector{2})); + ASSERT_EQ(values(a), (std::vector{1, 3})); +} + +TEST(intrusive_list_splice, sub_range) { + vk::intrusive::list> a; + vk::intrusive::list> b; + std::array, 5> ns{vk::intrusive::list_node{1}, vk::intrusive::list_node{2}, vk::intrusive::list_node{3}, + vk::intrusive::list_node{4}, vk::intrusive::list_node{5}}; + + for (auto& n : ns) { + a.push_back(n); + } + // move [2, 4) -> nodes 2 and 3 + auto first = std::next(a.begin()); + auto last = std::next(a.begin(), 3); + b.splice(b.end(), a, first, last); + + ASSERT_EQ(values(b), (std::vector{2, 3})); + ASSERT_EQ(values(a), (std::vector{1, 4, 5})); +} + +TEST(intrusive_list_splice, empty_other_is_noop) { + vk::intrusive::list> a; + vk::intrusive::list> b; + vk::intrusive::list_node b1{1}, b2{2}; + + b.push_back(b1); + b.push_back(b2); + + b.splice(b.begin(), a); + + ASSERT_EQ(values(b), (std::vector{1, 2})); + ASSERT_TRUE(a.empty()); +} + +TEST(intrusive_list_splice, within_same_list_moves_node) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}, c{3}; + + l.push_back(a); + l.push_back(b); + l.push_back(c); + // move node `2` to the front + l.splice(l.begin(), l, std::next(l.begin())); + + ASSERT_EQ(values(l), (std::vector{2, 1, 3})); + ASSERT_EQ(l.size(), 3); +} + +TEST(intrusive_list_splice, self_splice_to_same_position_is_noop) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.push_back(a); + l.push_back(b); + // splicing the first node to the position right after it (== its own place) is a noop + l.splice(std::next(l.begin()), l, l.begin()); + + ASSERT_EQ(values(l), (std::vector{1, 2})); + + // splicing the first node to the position right before it (== its own place) is a noop + l.splice(l.begin(), l, l.begin()); + + ASSERT_EQ(values(l), (std::vector{1, 2})); +} + +TEST(intrusive_list_splice, rvalue_overloads) { + vk::intrusive::list> a; + vk::intrusive::list> b; + vk::intrusive::list_node a1{1}, a2{2}; + + a.push_back(a1); + a.push_back(a2); + + b.splice(b.end(), std::move(a)); + + ASSERT_EQ(values(b), (std::vector{1, 2})); + ASSERT_TRUE(a.empty()); + + a.splice(a.begin(), std::move(b), b.begin()); + + ASSERT_EQ(values(a), (std::vector{1})); + ASSERT_EQ(values(b), (std::vector{2})); + + a.splice(a.begin(), std::move(b), b.begin(), std::next(b.begin())); + + ASSERT_EQ(values(a), (std::vector{2, 1})); + ASSERT_TRUE(b.empty()); +} + +TEST(intrusive_list_move, move_construct_transfers_nodes) { + vk::intrusive::list> src; + std::array, 3> ns{vk::intrusive::list_node{1}, vk::intrusive::list_node{2}, vk::intrusive::list_node{3}}; + + for (auto& n : ns) { + src.push_back(n); + } + vk::intrusive::list> dst{std::move(src)}; + + ASSERT_EQ(values(dst), (std::vector{1, 2, 3})); + ASSERT_TRUE(src.empty()); +} + +TEST(intrusive_list_move, move_assign_into_empty) { + vk::intrusive::list> src; + vk::intrusive::list_node a{1}, b{2}; + + src.push_back(a); + src.push_back(b); + + vk::intrusive::list> dst; + + dst = std::move(src); + + ASSERT_EQ(values(dst), (std::vector{1, 2})); + ASSERT_TRUE(src.empty()); +} + +TEST(intrusive_list_move, move_assign) { + vk::intrusive::list> src; + vk::intrusive::list_node a{1}, b{2}, c{3}, d{4}; + + src.push_back(a); + src.push_back(b); + + vk::intrusive::list> dst; + + dst.push_back(c); + dst.push_back(d); + dst = std::move(src); + + ASSERT_EQ(values(dst), (std::vector{1, 2})); + ASSERT_TRUE(src.empty()); +} + +TEST(intrusive_list_move, move_linked_node) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.push_back(a); + l.push_back(b); + + vk::intrusive::list_node c{std::move(a)}; + + ASSERT_EQ(values(l), (std::vector{1, 2})); + + ASSERT_EQ(std::addressof(c.value()), std::addressof(*l.begin())); +} + +TEST(intrusive_list_move, move_assign_unlinked_node) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.push_back(a); + l.push_back(b); + + vk::intrusive::list_node c{0}; + c = std::move(a); + + ASSERT_EQ(values(l), (std::vector{1, 2})); + ASSERT_EQ(std::addressof(l.front()), std::addressof(c.value())); +} + +TEST(intrusive_list_move, move_assign_linked_node) { + vk::intrusive::list> l1; + vk::intrusive::list> l2; + vk::intrusive::list_node a{1}, b{2}, c{3}; + + l1.push_back(a); + l1.push_back(b); + l2.push_back(c); + + // 'c' is already linked in l2; assigning into it must first unlink it from l2, + // then take over 'a's place in l1. + c = std::move(a); + + ASSERT_TRUE(l2.empty()); + ASSERT_EQ(values(l1), (std::vector{1, 2})); + ASSERT_EQ(std::addressof(l1.front()), std::addressof(c.value())); +} + +TEST(intrusive_list_move, self_move_assign_node_is_noop) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}; + + l.push_back(a); + + auto* self = std::addressof(a); // to avoid compilation error [-Wself-move] + a = std::move(*self); + + ASSERT_EQ(values(l), (std::vector{1})); + ASSERT_EQ(std::addressof(l.front()), std::addressof(a.value())); +} + +TEST(intrusive_list_move, self_move_assign_is_noop) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.push_back(a); + l.push_back(b); + + auto* self = std::addressof(l); // to avoid compilation error [-Wself-move] + l = std::move(*self); + + ASSERT_EQ(values(l), (std::vector{1, 2})); +} + +TEST(intrusive_list_value, stores_move_only_value_type) { + vk::intrusive::list>> l; + vk::intrusive::list_node> a{std::make_unique(1)}; + vk::intrusive::list_node> b{std::make_unique(2)}; + + l.push_back(a); + l.push_back(b); + + auto it = l.begin(); + + ASSERT_EQ(**it, 1); + + ++it; + + ASSERT_EQ(**it, 2); +} + +TEST(intrusive_list_value, make_list_node_helper) { + auto n = vk::intrusive::make_list_node(123); + static_assert(std::is_same_v); + vk::intrusive::list> l; + + l.push_back(n); + + ASSERT_EQ(l.front(), 123); +} + +TEST(intrusive_list_iterator_to, single_node) { + vk::intrusive::list> l; + vk::intrusive::list_node a{42}; + + l.push_back(a); + + auto it = l.iterator_to(a); + + ASSERT_EQ(it, l.begin()); + ASSERT_EQ(*it, 42); +} + +TEST(intrusive_list_iterator_to, points_at_correct_position_in_middle) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}, c{3}; + + l.push_back(a); + l.push_back(b); + l.push_back(c); + + auto it = l.iterator_to(b); + + ASSERT_EQ(*it, 2); + ASSERT_EQ(it, std::next(l.begin())); + ASSERT_EQ(*std::prev(it), 1); + ASSERT_EQ(*std::next(it), 3); +} + +TEST(intrusive_list_iterator_to, works_for_front_and_back) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}, c{3}; + + l.push_back(a); + l.push_back(b); + l.push_back(c); + + ASSERT_EQ(l.iterator_to(a), l.begin()); + ASSERT_EQ(l.iterator_to(c), std::prev(l.end())); +} + +TEST(intrusive_list_iterator_to, const_overload) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.push_back(a); + l.push_back(b); + const auto& cl = l; + + auto cit = cl.iterator_to(b); + + ASSERT_EQ(*cit, 2); + ASSERT_EQ(cit, std::next(cl.cbegin())); +} + +TEST(intrusive_list_iterator_to, mutating_through_returned_iterator_is_visible_via_node) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}; + + l.push_back(a); + + auto it = l.iterator_to(a); + *it = 100; + + ASSERT_EQ(a.value(), 100); +} + +TEST(intrusive_list_iterator_to, remains_valid_after_owning_node_is_relocated) { + vk::intrusive::list> l; + vk::intrusive::list_node a{1}, b{2}; + + l.push_back(a); + l.push_back(b); + + auto relocated = std::make_unique>(std::move(a)); + auto it = l.iterator_to(*relocated); + + ASSERT_EQ(*it, 1); + ASSERT_EQ(it, l.begin()); + ASSERT_EQ(values(l), (std::vector{1, 2})); +} + +namespace { + +struct tag_a {}; +struct tag_b {}; + +} // namespace + +TEST(intrusive_list_tags, node_lives_in_two_lists_at_once) { + vk::intrusive::list, tag_a> la; + vk::intrusive::list, tag_b> lb; + vk::intrusive::list_node n1{1}, n2{2}, n3{3}; + + la.push_back(n1); + la.push_back(n2); + la.push_back(n3); + + lb.push_back(n3); + lb.push_back(n2); + lb.push_back(n1); + + ASSERT_EQ(values(la), (std::vector{1, 2, 3})); + ASSERT_EQ(values(lb), (std::vector{3, 2, 1})); +} + +TEST(intrusive_list_tags, erase_from_one_list_keeps_the_other) { + vk::intrusive::list, tag_a> la; + vk::intrusive::list, tag_b> lb; + vk::intrusive::list_node n1{1}, n2{2}, n3{3}; + + la.push_back(n1); + la.push_back(n2); + la.push_back(n3); + lb.push_back(n1); + lb.push_back(n2); + lb.push_back(n3); + + la.erase(la.begin()); // remove n1 from `la` only + + ASSERT_EQ(values(la), (std::vector{2, 3})); + ASSERT_EQ(values(lb), (std::vector{1, 2, 3})); +} + +TEST(intrusive_list_tags, mutation_through_one_tag_visible_via_other) { + vk::intrusive::list, tag_a> la; + vk::intrusive::list, tag_b> lb; + vk::intrusive::list_node n{5}; + + la.push_back(n); + lb.push_back(n); + + la.front() = 99; + + ASSERT_EQ(lb.front(), 99); +} + +TEST(intrusive_list_tags, move_linked_node_with_two_tags) { + vk::intrusive::list, tag_a> la; + vk::intrusive::list, tag_b> lb; + vk::intrusive::list_node n{5}; + + la.push_back(n); + lb.push_back(n); + + vk::intrusive::list_node new_n{std::move(n)}; + + ASSERT_EQ(values(la), (std::vector{5})); + ASSERT_EQ(values(lb), (std::vector{5})); + ASSERT_EQ(la.iterator_to(new_n), la.begin()); + ASSERT_EQ(lb.iterator_to(new_n), lb.begin()); +} + +TEST(intrusive_list_tags, move_assign_linked_node_with_two_tags) { + vk::intrusive::list, tag_a> la; + vk::intrusive::list, tag_b> lb; + vk::intrusive::list_node n{5}; + vk::intrusive::list_node dst{7}; + + la.push_back(n); + lb.push_back(n); + + dst = std::move(n); + + ASSERT_EQ(values(la), (std::vector{5})); + ASSERT_EQ(values(lb), (std::vector{5})); + ASSERT_EQ(la.iterator_to(dst), la.begin()); + ASSERT_EQ(lb.iterator_to(dst), lb.begin()); +} diff --git a/common/containers/intrusive-list.h b/common/containers/intrusive-list.h new file mode 100644 index 0000000000..eef7e420b7 --- /dev/null +++ b/common/containers/intrusive-list.h @@ -0,0 +1,491 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "common/type_traits/apply_tuple.h" + +namespace vk::intrusive { + +template +class list; + +template +class list_iterator; + +namespace details { + +class list_node_base { + list_node_base* m_prev{this}; + list_node_base* m_next{this}; + + auto take_place_of(list_node_base&& other) noexcept -> void { + if (other.is_linked()) { + m_prev = std::exchange(other.m_prev, std::addressof(other)); + m_next = std::exchange(other.m_next, std::addressof(other)); + + m_prev->m_next = this; + m_next->m_prev = this; + } + } + + template + friend class vk::intrusive::list; + + template + friend class vk::intrusive::list_iterator; + +public: + list_node_base() noexcept = default; + + list_node_base(const list_node_base& /*unused*/) noexcept = delete; + + list_node_base(list_node_base&& other) noexcept { + take_place_of(std::move(other)); + } + + auto operator=(const list_node_base& /*unused*/) noexcept -> list_node_base& = delete; + + auto operator=(list_node_base&& other) noexcept -> list_node_base& { + if (this != std::addressof(other)) { + unlink(); + take_place_of(std::move(other)); + } + + return *this; + } + + ~list_node_base() { + unlink(); + } + + auto is_linked() const noexcept -> bool { + return m_prev != this; + } + + auto unlink() noexcept -> void { + m_prev->m_next = m_next; + m_next->m_prev = m_prev; + + m_prev = this; + m_next = this; + } +}; + +template +struct tagged_hook : public list_node_base {}; + +template +struct tagged_hooks : public tagged_hook... {}; + +} // namespace details + +struct default_tag {}; + +template +class list_node final : private std::conditional_t, details::tagged_hooks> { + T m_value{}; + + template + friend class vk::intrusive::list; + + template + friend class vk::intrusive::list_iterator; + +public: + using value_type = T; + using tags = std::conditional_t, std::tuple>; + + list_node() noexcept = default; + + explicit list_node(T value) noexcept + : m_value{std::move(value)} {} + + list_node(const list_node& other) = delete; + + list_node(list_node&& other) noexcept = default; + + auto operator=(const list_node& other) -> list_node& = delete; + + auto operator=(list_node&& other) noexcept -> list_node& = default; + + ~list_node() = default; + + auto value() noexcept -> T& { + return m_value; + } + + auto value() const noexcept -> const T& { + return m_value; + } +}; + +template +auto make_list_node(T value) noexcept -> list_node { + return list_node{std::move(value)}; +} + +namespace details { + +template +struct is_list_node : std::false_type {}; + +template +struct is_list_node> : std::true_type {}; + +template +inline constexpr bool is_list_node_v = is_list_node>::value; + +template +struct is_tag_of : std::false_type {}; + +template +struct is_tag_of> : std::bool_constant<(std::is_same_v || ...)> {}; + +template +inline constexpr bool is_tag_of_v = is_tag_of::tags>::value; + +} // namespace details + +template +class list_iterator { +public: + using difference_type = std::ptrdiff_t; + using value_type = typename Node::value_type; + using pointer = std::conditional_t, const value_type*, value_type*>; + using reference = std::conditional_t, const value_type&, value_type&>; + using iterator_category = std::bidirectional_iterator_tag; + +private: + static_assert(details::is_list_node_v, "Node must be a specialization of list_node"); + static_assert(details::is_tag_of_v, "Tag is not one of Node's tags"); + + details::list_node_base* m_curr{nullptr}; + + explicit list_iterator(const details::list_node_base* node) noexcept + : m_curr{const_cast(node)} {} + + static auto value_from_list_node_base(details::list_node_base* node) noexcept -> reference { + using tagged_hooks_t = vk::apply_tuple_t; + /* + * save cast to local variable to avoid compilation error in g++-11: + * error: ‘this’ pointer is null [-Werror=nonnull] + * 150 | return (static_cast(static_cast(static_cast*>(node))))->value(); + */ + auto* list_node{static_cast(static_cast(static_cast*>(node)))}; + return list_node->value(); + } + + template + friend class vk::intrusive::list_iterator; + + template + friend class vk::intrusive::list; + +public: + list_iterator() noexcept = default; + + list_iterator(const list_iterator& other) noexcept = default; + + template && !std::is_const_v>> + list_iterator(const list_iterator& other) noexcept // NOLINT (hicpp-explicit-conversions) + : m_curr{other.m_curr} {} + + list_iterator(list_iterator&& other) noexcept = default; + + auto operator=(const list_iterator& other) noexcept -> list_iterator& = default; + + auto operator=(list_iterator&& other) noexcept -> list_iterator& = default; + + ~list_iterator() = default; + + auto operator++() noexcept -> list_iterator& { + m_curr = m_curr->m_next; + return *this; + } + + auto operator++(int) noexcept -> list_iterator { + list_iterator res{*this}; + ++*this; + return res; + } + + auto operator--() noexcept -> list_iterator& { + m_curr = m_curr->m_prev; + return *this; + } + + auto operator--(int) noexcept -> list_iterator { + list_iterator res{*this}; + --*this; + return res; + } + + auto operator*() const noexcept -> reference { + return value_from_list_node_base(m_curr); + } + + auto operator->() const noexcept -> pointer { + return std::addressof(value_from_list_node_base(m_curr)); + } + + template, std::remove_const_t>>> + auto operator==(const list_iterator& other) const noexcept -> bool { + return m_curr == other.m_curr; + } + + template, std::remove_const_t>>> + auto operator!=(const list_iterator& other) const noexcept -> bool { + return !(*this == other); + } +}; + +template +class list { +public: + using value_type = typename Node::value_type; + using size_type = size_t; + using difference_type = ptrdiff_t; + using reference = value_type&; + using const_reference = const value_type&; + using pointer = value_type*; + using const_pointer = const value_type*; + using iterator = list_iterator; + using const_iterator = list_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + +private: + static_assert(details::is_list_node_v, "Node must be a specialization of list_node"); + static_assert(details::is_tag_of_v, "Tag is not one of Node's tags"); + + details::list_node_base m_sentinel; + + static auto list_node_base_from_list_node(Node& node) noexcept -> details::list_node_base* { + using tagged_hooks_t = vk::apply_tuple_t; + return static_cast(static_cast*>(static_cast(std::addressof(node)))); + } + + static auto list_node_base_from_list_node(const Node& node) noexcept -> const details::list_node_base* { + using tagged_hooks_t = vk::apply_tuple_t; + return static_cast(static_cast*>(static_cast(std::addressof(node)))); + } + + auto splice_impl(const_iterator pos, list& other, const_iterator first, const_iterator last) noexcept -> void { + if (first == last || (this == std::addressof(other) && (first == pos || last == pos))) { + return; + } + + auto* pos_node{pos.m_curr}; + auto* first_node{first.m_curr}; + auto* last_node{last.m_curr->m_prev}; + + first_node->m_prev->m_next = last_node->m_next; + last_node->m_next->m_prev = first_node->m_prev; + + first_node->m_prev = pos_node->m_prev; + last_node->m_next = pos_node; + + pos_node->m_prev->m_next = first_node; + pos_node->m_prev = last_node; + } + +public: + list() noexcept = default; + + list(const list& other) = delete; + + list(list&& other) noexcept = default; + + auto operator=(const list& other) -> list& = delete; + + auto operator=(list&& other) noexcept -> list& = default; + + ~list() = default; + + auto front() noexcept -> reference { + return *begin(); + } + + auto front() const noexcept -> const_reference { + return *begin(); + } + + auto back() noexcept -> reference { + return *std::prev(end()); + } + + auto back() const noexcept -> const_reference { + return *std::prev(end()); + } + + auto begin() noexcept -> iterator { + return iterator{m_sentinel.m_next}; + } + + auto begin() const noexcept -> const_iterator { + return const_iterator{m_sentinel.m_next}; + } + + auto cbegin() const noexcept -> const_iterator { + return begin(); + } + + auto end() noexcept -> iterator { + return iterator{std::addressof(m_sentinel)}; + } + + auto end() const noexcept -> const_iterator { + return const_iterator{std::addressof(m_sentinel)}; + } + + auto cend() const noexcept -> const_iterator { + return end(); + } + + auto rbegin() noexcept -> reverse_iterator { + return reverse_iterator{end()}; + } + + auto rbegin() const noexcept -> const_reverse_iterator { + return const_reverse_iterator{end()}; + } + + auto crbegin() const noexcept -> const_reverse_iterator { + return rbegin(); + } + + auto rend() noexcept -> reverse_iterator { + return reverse_iterator{begin()}; + } + + auto rend() const noexcept -> const_reverse_iterator { + return const_reverse_iterator{begin()}; + } + + auto crend() const noexcept -> const_reverse_iterator { + return rend(); + } + + auto empty() const noexcept -> bool { + return !m_sentinel.is_linked(); + } + + // complexity O(n) + auto size() const noexcept -> size_type { + return std::distance(begin(), end()); + } + + auto clear() noexcept -> void { + m_sentinel.unlink(); + } + + // if the node is linked in list with the same tag, it will be unlinked from list before insertion + auto insert(const_iterator pos, Node& node) noexcept -> iterator { + auto* next_node{pos.m_curr}; + auto* new_node{list_node_base_from_list_node(node)}; + if (new_node == next_node) { + return iterator{next_node}; + } + + new_node->unlink(); + + new_node->m_prev = next_node->m_prev; + new_node->m_next = next_node; + + new_node->m_prev->m_next = new_node; + new_node->m_next->m_prev = new_node; + + return iterator{new_node}; + } + + auto erase(const_iterator pos) noexcept -> iterator { + auto* remove_node{pos.m_curr}; + auto* next_node{remove_node->m_next}; + + remove_node->unlink(); + + return iterator{next_node}; + } + + auto erase(const_iterator first, const_iterator last) noexcept -> iterator { + while (first != last) { + first = erase(first); + } + + return iterator{last.m_curr}; + } + + auto iterator_to(Node& node) noexcept -> iterator { + return iterator{list_node_base_from_list_node(node)}; + } + + auto iterator_to(const Node& node) const noexcept -> const_iterator { + return const_iterator{list_node_base_from_list_node(node)}; + } + + auto push_back(Node& node) noexcept -> void { + insert(end(), node); + } + + auto push_front(Node& node) noexcept -> void { + insert(begin(), node); + } + + auto pop_back() noexcept -> void { + erase(std::prev(end())); + } + + auto pop_front() noexcept -> void { + erase(begin()); + } + + auto swap(list& other) noexcept -> void { + if (this == std::addressof(other)) { + return; + } + + details::list_node_base tmp{std::move(m_sentinel)}; + m_sentinel = std::move(other.m_sentinel); + other.m_sentinel = std::move(tmp); + } + + auto splice(const_iterator pos, list& other) noexcept -> void { + splice(pos, other, other.begin(), other.end()); + } + + auto splice(const_iterator pos, list&& other) noexcept -> void { + splice(pos, other, other.begin(), other.end()); + } + + auto splice(const_iterator pos, list& other, const_iterator it) noexcept -> void { + splice(pos, other, it, std::next(it)); + } + + auto splice(const_iterator pos, list&& other, const_iterator it) noexcept -> void { + splice(pos, other, it, std::next(it)); + } + + auto splice(const_iterator pos, list& other, const_iterator first, const_iterator last) noexcept -> void { + splice_impl(pos, other, first, last); + } + + auto splice(const_iterator pos, list&& other, const_iterator first, const_iterator last) noexcept -> void { + splice_impl(pos, other, first, last); + } +}; + +template +auto swap(list& lhs, list& rhs) noexcept -> void { + lhs.swap(rhs); +} + +} // namespace vk::intrusive diff --git a/common/type_traits/apply_tuple.h b/common/type_traits/apply_tuple.h new file mode 100644 index 0000000000..96d97206a6 --- /dev/null +++ b/common/type_traits/apply_tuple.h @@ -0,0 +1,22 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2026 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include + +namespace vk { + +template typename Template, typename Tuple> +struct apply_tuple; + +template typename Template, typename... Args> +struct apply_tuple> { + using type = Template; +}; + +template typename Template, typename Tuple> +using apply_tuple_t = typename apply_tuple::type; + +} // namespace vk diff --git a/runtime-common/core/std/containers.h b/runtime-common/core/std/containers.h index d0b2563973..f8d0ec549c 100644 --- a/runtime-common/core/std/containers.h +++ b/runtime-common/core/std/containers.h @@ -4,6 +4,7 @@ #pragma once +#include "common/containers/intrusive-list.h" #include #include #include @@ -50,6 +51,18 @@ using stack = std::stack>; template class Allocator> using string = std::basic_string, Allocator>; +namespace intrusive { + +template +using list_node = vk::intrusive::list_node; + +template +using list = vk::intrusive::list; + +using vk::intrusive::make_list_node; + +} // namespace intrusive + } // namespace stl } // namespace kphp