util/IntrusiveTreeSet: new class
This commit is contained in:
parent
6a99f20828
commit
5a0bad3b2f
498
src/util/IntrusiveTreeSet.hxx
Normal file
498
src/util/IntrusiveTreeSet.hxx
Normal file
@ -0,0 +1,498 @@
|
|||||||
|
// SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
// Copyright CM4all GmbH
|
||||||
|
// author: Max Kellermann <mk@cm4all.com>
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RedBlackTree.hxx"
|
||||||
|
|
||||||
|
#include "Cast.hxx"
|
||||||
|
#include "Concepts.hxx"
|
||||||
|
#include "IntrusiveHookMode.hxx"
|
||||||
|
#include "MemberPointer.hxx"
|
||||||
|
#include "OptionalCounter.hxx"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <compare> // for std::weak_ordering
|
||||||
|
#include <concepts> // for std::regular_invocable
|
||||||
|
#include <optional>
|
||||||
|
#include <utility> // for std::exchange()
|
||||||
|
|
||||||
|
struct IntrusiveTreeSetOptions {
|
||||||
|
bool constant_time_size = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<IntrusiveHookMode _mode=IntrusiveHookMode::NORMAL>
|
||||||
|
class IntrusiveTreeSetHook {
|
||||||
|
template<typename T> friend struct IntrusiveTreeSetBaseHookTraits;
|
||||||
|
template<auto member> friend struct IntrusiveTreeSetMemberHookTraits;
|
||||||
|
template<typename T, typename Operators, typename HookTraits, IntrusiveTreeSetOptions> friend class IntrusiveTreeSet;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
RedBlackTreeNode node;
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr IntrusiveHookMode mode = _mode;
|
||||||
|
|
||||||
|
constexpr IntrusiveTreeSetHook() noexcept {
|
||||||
|
if constexpr (mode >= IntrusiveHookMode::TRACK)
|
||||||
|
node.parent = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr ~IntrusiveTreeSetHook() noexcept {
|
||||||
|
if constexpr (mode >= IntrusiveHookMode::AUTO_UNLINK)
|
||||||
|
if (is_linked())
|
||||||
|
unlink();
|
||||||
|
}
|
||||||
|
|
||||||
|
IntrusiveTreeSetHook(const IntrusiveTreeSetHook &) = delete;
|
||||||
|
IntrusiveTreeSetHook &operator=(const IntrusiveTreeSetHook &) = delete;
|
||||||
|
|
||||||
|
constexpr void unlink() noexcept {
|
||||||
|
if constexpr (mode >= IntrusiveHookMode::TRACK) {
|
||||||
|
assert(is_linked());
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Unlink();
|
||||||
|
|
||||||
|
if constexpr (mode >= IntrusiveHookMode::TRACK)
|
||||||
|
node.parent = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_linked() const noexcept {
|
||||||
|
static_assert(mode >= IntrusiveHookMode::TRACK);
|
||||||
|
|
||||||
|
return node.parent != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr auto &Cast(RedBlackTreeNode &node) noexcept {
|
||||||
|
return ContainerCast(node, &IntrusiveTreeSetHook::node);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr const auto &Cast(const RedBlackTreeNode &node) noexcept {
|
||||||
|
return ContainerCast(node, &IntrusiveTreeSetHook::node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect the hook type.
|
||||||
|
*/
|
||||||
|
template<typename U>
|
||||||
|
struct IntrusiveTreeSetHookDetection {
|
||||||
|
/* TODO can this be simplified somehow, without checking for
|
||||||
|
all possible enum values? */
|
||||||
|
using type = std::conditional_t<std::is_base_of_v<IntrusiveTreeSetHook<IntrusiveHookMode::NORMAL>, U>,
|
||||||
|
IntrusiveTreeSetHook<IntrusiveHookMode::NORMAL>,
|
||||||
|
std::conditional_t<std::is_base_of_v<IntrusiveTreeSetHook<IntrusiveHookMode::TRACK>, U>,
|
||||||
|
IntrusiveTreeSetHook<IntrusiveHookMode::TRACK>,
|
||||||
|
std::conditional_t<std::is_base_of_v<IntrusiveTreeSetHook<IntrusiveHookMode::AUTO_UNLINK>, U>,
|
||||||
|
IntrusiveTreeSetHook<IntrusiveHookMode::AUTO_UNLINK>,
|
||||||
|
void>>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For classes which embed #IntrusiveTreeSetHook as base class.
|
||||||
|
*/
|
||||||
|
template<typename T>
|
||||||
|
struct IntrusiveTreeSetBaseHookTraits {
|
||||||
|
template<typename U>
|
||||||
|
using Hook = typename IntrusiveTreeSetHookDetection<U>::type;
|
||||||
|
|
||||||
|
static constexpr T *Cast(RedBlackTreeNode *node) noexcept {
|
||||||
|
auto *hook = &Hook<T>::Cast(*node);
|
||||||
|
return static_cast<T *>(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr auto &ToHook(T &t) noexcept {
|
||||||
|
return static_cast<Hook<T> &>(t);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For classes which embed #IntrusiveTreeSetHook as member.
|
||||||
|
*/
|
||||||
|
template<auto member>
|
||||||
|
struct IntrusiveTreeSetMemberHookTraits {
|
||||||
|
using T = MemberPointerContainerType<decltype(member)>;
|
||||||
|
using _Hook = MemberPointerType<decltype(member)>;
|
||||||
|
|
||||||
|
template<typename Dummy>
|
||||||
|
using Hook = _Hook;
|
||||||
|
|
||||||
|
static constexpr T *Cast(RedBlackTreeNode *node) noexcept {
|
||||||
|
auto &hook = Hook<T>::Cast(*node);
|
||||||
|
return &ContainerCast(hook, member);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr auto &ToHook(T &t) noexcept {
|
||||||
|
return t.*member;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param GetKey a function object which extracts the "key" part of an
|
||||||
|
* item
|
||||||
|
*/
|
||||||
|
template<typename T,
|
||||||
|
std::regular_invocable<const T &> GetKey=std::identity,
|
||||||
|
std::regular_invocable<std::invoke_result_t<GetKey, const T &>,
|
||||||
|
std::invoke_result_t<GetKey, const T &>> Compare=std::compare_three_way>
|
||||||
|
struct IntrusiveTreeSetOperators {
|
||||||
|
[[no_unique_address]]
|
||||||
|
GetKey get_key;
|
||||||
|
|
||||||
|
[[no_unique_address]]
|
||||||
|
Compare compare;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A binary tree implementation which stores pointers to items which
|
||||||
|
* have an embedded #IntrusiveTreeSetHook.
|
||||||
|
*/
|
||||||
|
template<typename T,
|
||||||
|
typename Operators=IntrusiveTreeSetOperators<T>,
|
||||||
|
typename HookTraits=IntrusiveTreeSetBaseHookTraits<T>,
|
||||||
|
IntrusiveTreeSetOptions options=IntrusiveTreeSetOptions{}>
|
||||||
|
class IntrusiveTreeSet {
|
||||||
|
static constexpr bool constant_time_size = options.constant_time_size;
|
||||||
|
|
||||||
|
[[no_unique_address]]
|
||||||
|
OptionalCounter<constant_time_size> counter;
|
||||||
|
|
||||||
|
[[no_unique_address]]
|
||||||
|
Operators ops;
|
||||||
|
|
||||||
|
RedBlackTreeNode head{RedBlackTreeNode::Head{}};
|
||||||
|
|
||||||
|
public:
|
||||||
|
using value_type = T;
|
||||||
|
using reference = T &;
|
||||||
|
using const_reference = const T &;
|
||||||
|
using pointer = T *;
|
||||||
|
using const_pointer = const T *;
|
||||||
|
using size_type = std::size_t;
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
IntrusiveTreeSet() noexcept = default;
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr bool empty() const noexcept {
|
||||||
|
return GetRoot() == nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr size_type size() const noexcept {
|
||||||
|
if constexpr (constant_time_size)
|
||||||
|
return counter;
|
||||||
|
else
|
||||||
|
return std::distance(begin(), end());
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void clear() noexcept {
|
||||||
|
SetRoot(nullptr);
|
||||||
|
counter.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void clear_and_dispose(Disposer<value_type> auto disposer) noexcept {
|
||||||
|
dispose_all(GetRoot(), disposer);
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
class iterator {
|
||||||
|
friend IntrusiveTreeSet;
|
||||||
|
|
||||||
|
RedBlackTreeNode *node;
|
||||||
|
|
||||||
|
public:
|
||||||
|
using iterator_category = std::bidirectional_iterator_tag;
|
||||||
|
using value_type = T;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
using pointer = value_type *;
|
||||||
|
using reference = value_type &;
|
||||||
|
|
||||||
|
explicit constexpr iterator(RedBlackTreeNode *_node) noexcept
|
||||||
|
:node(_node) {}
|
||||||
|
|
||||||
|
constexpr bool operator==(const iterator &) const noexcept = default;
|
||||||
|
constexpr bool operator!=(const iterator &) const noexcept = default;
|
||||||
|
|
||||||
|
constexpr reference operator*() const noexcept {
|
||||||
|
return *Cast(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr pointer operator->() const noexcept {
|
||||||
|
return Cast(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto &operator++() noexcept {
|
||||||
|
node = RedBlackTreeNode::GetNextNode(node);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr iterator begin() noexcept {
|
||||||
|
auto *root = GetRoot();
|
||||||
|
return root != nullptr
|
||||||
|
? iterator{&root->GetLeftMost()}
|
||||||
|
: end();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr iterator end() noexcept {
|
||||||
|
return iterator{nullptr};
|
||||||
|
}
|
||||||
|
|
||||||
|
class const_iterator {
|
||||||
|
friend IntrusiveTreeSet;
|
||||||
|
|
||||||
|
const RedBlackTreeNode *node;
|
||||||
|
|
||||||
|
public:
|
||||||
|
using iterator_category = std::bidirectional_iterator_tag;
|
||||||
|
using value_type = const T;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
using pointer = value_type *;
|
||||||
|
using reference = value_type &;
|
||||||
|
|
||||||
|
explicit constexpr const_iterator(RedBlackTreeNode *_node) noexcept
|
||||||
|
:node(_node) {}
|
||||||
|
|
||||||
|
constexpr const_iterator(iterator i) noexcept
|
||||||
|
:node(i.node) {}
|
||||||
|
|
||||||
|
constexpr bool operator==(const const_iterator &) const noexcept = default;
|
||||||
|
constexpr bool operator!=(const const_iterator &) const noexcept = default;
|
||||||
|
|
||||||
|
constexpr reference operator*() const noexcept {
|
||||||
|
return *Cast(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr pointer operator->() const noexcept {
|
||||||
|
return Cast(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto &operator++() noexcept {
|
||||||
|
node = RedBlackTreeNode::GetNextNode(const_cast<RedBlackTreeNode *>(node));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr const_iterator begin() const noexcept {
|
||||||
|
auto *root = GetRoot();
|
||||||
|
return root != nullptr
|
||||||
|
? const_iterator{&root->GetLeftMost()}
|
||||||
|
: end();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr const_iterator end() const noexcept {
|
||||||
|
return const_iterator{nullptr};
|
||||||
|
}
|
||||||
|
|
||||||
|
const_reference front() const noexcept {
|
||||||
|
auto i = begin();
|
||||||
|
assert(i != end());
|
||||||
|
|
||||||
|
return *i;
|
||||||
|
}
|
||||||
|
|
||||||
|
reference front() noexcept {
|
||||||
|
auto i = begin();
|
||||||
|
assert(i != end());
|
||||||
|
|
||||||
|
return *i;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
static constexpr iterator iterator_to(reference item) noexcept {
|
||||||
|
return iterator{&ToNode(item)};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr iterator find(const auto &key) const noexcept {
|
||||||
|
auto *node = GetRoot();
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
bool previous_red = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
while (node != nullptr) {
|
||||||
|
#ifndef NDEBUG
|
||||||
|
const bool current_red = node->color == RedBlackTreeNode::Color::RED;
|
||||||
|
assert(!previous_red || !current_red);
|
||||||
|
previous_red = current_red;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const auto &item = *Cast(node);
|
||||||
|
|
||||||
|
const std::weak_ordering compare_result = ops.compare(key, ops.get_key(item));
|
||||||
|
if (compare_result == std::weak_ordering::less)
|
||||||
|
node = node->GetLeft();
|
||||||
|
else if (compare_result == std::weak_ordering::greater)
|
||||||
|
node = node->GetRight();
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return iterator{node};
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr iterator insert(reference value) noexcept {
|
||||||
|
static_assert(!constant_time_size ||
|
||||||
|
GetHookMode() < IntrusiveHookMode::AUTO_UNLINK,
|
||||||
|
"Can't use auto-unlink hooks with constant_time_size");
|
||||||
|
|
||||||
|
auto *root = GetRoot();
|
||||||
|
if (root == nullptr) {
|
||||||
|
root = &ToNode(value);
|
||||||
|
root->Init(RedBlackTreeNode::Color::BLACK);
|
||||||
|
} else {
|
||||||
|
root = &insert(root, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetRoot(root);
|
||||||
|
|
||||||
|
return iterator_to(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
iterator erase(iterator i) noexcept {
|
||||||
|
assert(i.node != nullptr);
|
||||||
|
assert(!empty());
|
||||||
|
|
||||||
|
auto *next = RedBlackTreeNode::GetNextNode(i.node);
|
||||||
|
Cast(i.node)->unlink();
|
||||||
|
return iterator{next};
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_front() noexcept {
|
||||||
|
erase(begin());
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]]
|
||||||
|
static constexpr auto GetHookMode() noexcept {
|
||||||
|
return HookTraits::template Hook<T>::mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
static constexpr pointer Cast(RedBlackTreeNode *node) noexcept {
|
||||||
|
return HookTraits::Cast(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
static constexpr const_pointer Cast(const RedBlackTreeNode *node) noexcept {
|
||||||
|
return HookTraits::Cast(const_cast<RedBlackTreeNode *>(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
static constexpr auto &ToHook(T &t) noexcept {
|
||||||
|
return HookTraits::ToHook(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
static constexpr const auto &ToHook(const T &t) noexcept {
|
||||||
|
return HookTraits::ToHook(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
static constexpr RedBlackTreeNode &ToNode(T &t) noexcept {
|
||||||
|
return ToHook(t).node;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
static constexpr const RedBlackTreeNode &ToNode(const T &t) noexcept {
|
||||||
|
return ToHook(t).node;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr RedBlackTreeNode *GetRoot() const noexcept {
|
||||||
|
return head.GetLeft();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr bool IsRoot(const RedBlackTreeNode &node) const noexcept {
|
||||||
|
return &node == GetRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void SetRoot(RedBlackTreeNode *root) noexcept {
|
||||||
|
head.SetChild(RedBlackTreeNode::Direction::LEFT, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::pure]]
|
||||||
|
constexpr RedBlackTreeNode::Direction GetInsertDirection(RedBlackTreeNode &parent,
|
||||||
|
const_reference new_value) const noexcept {
|
||||||
|
const auto &parent_value = *Cast(&parent);
|
||||||
|
const std::weak_ordering compare_result = ops.compare(ops.get_key(new_value), ops.get_key(parent_value));
|
||||||
|
return compare_result == std::weak_ordering::less
|
||||||
|
? RedBlackTreeNode::Direction::LEFT
|
||||||
|
: RedBlackTreeNode::Direction::RIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<RedBlackTreeNode::Direction> rotate1, rotate2;
|
||||||
|
|
||||||
|
RedBlackTreeNode &insert(RedBlackTreeNode *base,
|
||||||
|
reference value) noexcept {
|
||||||
|
if (base == nullptr) {
|
||||||
|
auto &node = ToNode(value);
|
||||||
|
node.Init(RedBlackTreeNode::Color::RED);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the actual insert is here */
|
||||||
|
const auto insert_direction = GetInsertDirection(*base, value);
|
||||||
|
auto &new_child = insert(base->GetChild(insert_direction), value);
|
||||||
|
base->SetChild(insert_direction, &new_child);
|
||||||
|
const bool red_red_conflict = !IsRoot(*base) &&
|
||||||
|
base->color == RedBlackTreeNode::Color::RED &&
|
||||||
|
new_child.color == RedBlackTreeNode::Color::RED;
|
||||||
|
|
||||||
|
/* rotate */
|
||||||
|
if (rotate1) {
|
||||||
|
base->SetChild(*rotate1, &base->GetChild(*rotate1)->Rotate(*rotate1));
|
||||||
|
rotate1.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rotate2) {
|
||||||
|
base->color = RedBlackTreeNode::Color::RED;
|
||||||
|
base = &base->Rotate(*rotate2);
|
||||||
|
base->color = RedBlackTreeNode::Color::BLACK;
|
||||||
|
rotate2.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (red_red_conflict) {
|
||||||
|
const auto direction = base->GetDirectionInParent();
|
||||||
|
const auto other_direction = RedBlackTreeNode::OtherDirection(direction);
|
||||||
|
|
||||||
|
if (auto *sibling = base->parent->GetChild(other_direction);
|
||||||
|
sibling != nullptr &&
|
||||||
|
sibling->color == RedBlackTreeNode::Color::RED) {
|
||||||
|
sibling->color = RedBlackTreeNode::Color::BLACK;
|
||||||
|
base->color = RedBlackTreeNode::Color::BLACK;
|
||||||
|
if (!IsRoot(*base->parent))
|
||||||
|
base->parent->color = RedBlackTreeNode::Color::RED;
|
||||||
|
} else if (const auto *other_child = base->GetChild(other_direction);
|
||||||
|
other_child != nullptr && other_child->color == RedBlackTreeNode::Color::RED) {
|
||||||
|
rotate1 = direction;
|
||||||
|
rotate2 = other_direction;
|
||||||
|
} else if (const auto *child = base->GetChild(direction);
|
||||||
|
child != nullptr && child->color == RedBlackTreeNode::Color::RED) {
|
||||||
|
rotate2 = other_direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return *base;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose_all(RedBlackTreeNode *node, Disposer<value_type> auto disposer) noexcept {
|
||||||
|
if (node == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (auto *i : node->children)
|
||||||
|
dispose_all(i, disposer);
|
||||||
|
|
||||||
|
disposer(Cast(node));
|
||||||
|
}
|
||||||
|
};
|
356
src/util/RedBlackTree.hxx
Normal file
356
src/util/RedBlackTree.hxx
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
// SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
// Copyright CM4all GmbH
|
||||||
|
// author: Max Kellermann <mk@cm4all.com>
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm> // for std::any_of()
|
||||||
|
#include <array>
|
||||||
|
#include <cassert>
|
||||||
|
#include <utility> // for std::exchange()
|
||||||
|
|
||||||
|
struct RedBlackTreeNode {
|
||||||
|
RedBlackTreeNode *parent;
|
||||||
|
|
||||||
|
enum class Direction : std::size_t { LEFT, RIGHT };
|
||||||
|
|
||||||
|
std::array<RedBlackTreeNode *, 2> children;
|
||||||
|
|
||||||
|
enum class Color { HEAD, BLACK, RED };
|
||||||
|
|
||||||
|
Color color;
|
||||||
|
|
||||||
|
constexpr RedBlackTreeNode() noexcept = default;
|
||||||
|
|
||||||
|
struct Head {};
|
||||||
|
explicit constexpr RedBlackTreeNode(Head) noexcept
|
||||||
|
:children({}),
|
||||||
|
color(Color::HEAD) {}
|
||||||
|
|
||||||
|
RedBlackTreeNode(const RedBlackTreeNode &) = delete;
|
||||||
|
RedBlackTreeNode &operator=(const RedBlackTreeNode &) = delete;
|
||||||
|
|
||||||
|
constexpr void Init(Color _color) noexcept {
|
||||||
|
children = {};
|
||||||
|
color = _color;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr bool IsHead() const noexcept {
|
||||||
|
return color == Color::HEAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr bool IsRoot() const noexcept {
|
||||||
|
assert(!IsHead());
|
||||||
|
|
||||||
|
return parent->IsHead();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
static constexpr Direction OtherDirection(Direction direction) noexcept {
|
||||||
|
return static_cast<Direction>(static_cast<std::size_t>(direction) ^ 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr RedBlackTreeNode *GetChild(Direction direction) const noexcept {
|
||||||
|
return children[static_cast<std::size_t>(direction)];
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr RedBlackTreeNode *GetLeft() const noexcept {
|
||||||
|
return GetChild(Direction::LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr RedBlackTreeNode *GetRight() const noexcept {
|
||||||
|
return GetChild(Direction::RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr RedBlackTreeNode *GetOtherChild(Direction direction) const noexcept {
|
||||||
|
return GetChild(OtherDirection(direction));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new child and return the old one.
|
||||||
|
*/
|
||||||
|
constexpr auto *SetChild(Direction direction,
|
||||||
|
RedBlackTreeNode *child) noexcept {
|
||||||
|
auto *old = std::exchange(children[static_cast<std::size_t>(direction)],
|
||||||
|
child);
|
||||||
|
if (child != nullptr)
|
||||||
|
child->parent = this;
|
||||||
|
|
||||||
|
return old;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto *SetChild(Direction direction,
|
||||||
|
RedBlackTreeNode &child) noexcept {
|
||||||
|
auto *old = std::exchange(children[static_cast<std::size_t>(direction)],
|
||||||
|
&child);
|
||||||
|
child.parent = this;
|
||||||
|
|
||||||
|
return old;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto *SetOtherChild(Direction direction,
|
||||||
|
RedBlackTreeNode *child) noexcept {
|
||||||
|
return SetChild(OtherDirection(direction), child);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr Direction GetChildDirection(const RedBlackTreeNode &child) const noexcept {
|
||||||
|
assert(child.parent == this);
|
||||||
|
assert(&child == GetChild(Direction::LEFT) ||
|
||||||
|
&child == GetChild(Direction::RIGHT));
|
||||||
|
|
||||||
|
return &child == GetChild(Direction::LEFT)
|
||||||
|
? Direction::LEFT
|
||||||
|
: Direction::RIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void ReplaceChild(RedBlackTreeNode &old_child,
|
||||||
|
RedBlackTreeNode *new_child) noexcept {
|
||||||
|
SetChild(GetChildDirection(old_child), new_child);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void ReplaceChild(RedBlackTreeNode &old_child,
|
||||||
|
RedBlackTreeNode &new_child) noexcept {
|
||||||
|
SetChild(GetChildDirection(old_child), new_child);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr Direction GetDirectionInParent() const noexcept {
|
||||||
|
assert(parent != nullptr);
|
||||||
|
assert(!IsHead());
|
||||||
|
|
||||||
|
return parent->GetChildDirection(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
auto &Rotate(RedBlackTreeNode::Direction direction) noexcept {
|
||||||
|
assert(!IsHead());
|
||||||
|
|
||||||
|
auto *x = GetOtherChild(direction);
|
||||||
|
assert(x != nullptr);
|
||||||
|
|
||||||
|
auto *y = x->SetChild(direction, this);
|
||||||
|
SetOtherChild(direction, y);
|
||||||
|
|
||||||
|
return *x;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RotateInParent(RedBlackTreeNode::Direction direction) noexcept {
|
||||||
|
assert(parent != nullptr);
|
||||||
|
assert(!IsHead());
|
||||||
|
|
||||||
|
auto &p = *parent;
|
||||||
|
const auto direction_in_parent = p.GetChildDirection(*this);
|
||||||
|
|
||||||
|
auto &new_node = Rotate(direction);
|
||||||
|
assert(new_node.parent == this);
|
||||||
|
|
||||||
|
assert(p.GetChild(direction_in_parent) == this);
|
||||||
|
p.SetChild(direction_in_parent, new_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr static RedBlackTreeNode &GetLeftMost(RedBlackTreeNode *node) noexcept {
|
||||||
|
assert(node != nullptr);
|
||||||
|
assert(!node->IsHead());
|
||||||
|
|
||||||
|
while (auto *left = node->GetChild(Direction::LEFT)) {
|
||||||
|
assert(left->parent == node);
|
||||||
|
node = left;
|
||||||
|
}
|
||||||
|
|
||||||
|
return *node;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr RedBlackTreeNode &GetLeftMost() noexcept {
|
||||||
|
return GetLeftMost(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr static RedBlackTreeNode *GetLeftHandedParent(RedBlackTreeNode *node) noexcept {
|
||||||
|
assert(node != nullptr);
|
||||||
|
assert(!node->IsHead());
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
assert(node->parent != nullptr);
|
||||||
|
auto &p = *node->parent;
|
||||||
|
if (p.IsHead())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
assert(node->color != RedBlackTreeNode::Color::RED ||
|
||||||
|
p.color != RedBlackTreeNode::Color::RED);
|
||||||
|
|
||||||
|
if (p.GetChildDirection(*node) == Direction::LEFT)
|
||||||
|
return &p;
|
||||||
|
|
||||||
|
node = &p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr static RedBlackTreeNode *GetNextNode(RedBlackTreeNode *node) noexcept {
|
||||||
|
assert(node != nullptr);
|
||||||
|
assert(!node->IsHead());
|
||||||
|
|
||||||
|
if (auto *right = node->GetChild(Direction::RIGHT)) {
|
||||||
|
assert(node->color != RedBlackTreeNode::Color::RED ||
|
||||||
|
right->color != RedBlackTreeNode::Color::RED);
|
||||||
|
return &right->GetLeftMost();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(node->parent != nullptr);
|
||||||
|
auto &p = *node->parent;
|
||||||
|
if (p.IsHead())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
if (p.GetChildDirection(*node) == Direction::LEFT)
|
||||||
|
return &p;
|
||||||
|
|
||||||
|
return GetLeftHandedParent(&p);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]]
|
||||||
|
constexpr bool HasTwoChildren() const noexcept {
|
||||||
|
return children[0] != nullptr && children[1] != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr RedBlackTreeNode *GetAnyChild() const noexcept {
|
||||||
|
return children[children[1] != nullptr];
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
constexpr void Unlink() noexcept {
|
||||||
|
assert(parent != nullptr);
|
||||||
|
assert(!IsHead());
|
||||||
|
|
||||||
|
if (HasTwoChildren()) {
|
||||||
|
/* swap with successor, because it, by
|
||||||
|
definition, doesn't have two children; the
|
||||||
|
rest of this method assumes we have exactly
|
||||||
|
one child or none */
|
||||||
|
|
||||||
|
auto &right = *GetRight();
|
||||||
|
auto &successor = right.GetLeftMost();
|
||||||
|
|
||||||
|
auto &p = *parent;
|
||||||
|
const auto direction_in_parent = p.GetChildDirection(*this);
|
||||||
|
|
||||||
|
successor.SetChild(Direction::LEFT, GetLeft());
|
||||||
|
SetChild(Direction::LEFT, nullptr);
|
||||||
|
SetChild(Direction::RIGHT, successor.GetRight());
|
||||||
|
|
||||||
|
if (&successor == &right) {
|
||||||
|
assert(successor.parent == this);
|
||||||
|
|
||||||
|
successor.SetChild(Direction::RIGHT, *this);
|
||||||
|
} else {
|
||||||
|
assert(successor.parent != this);
|
||||||
|
|
||||||
|
successor.parent->SetChild(Direction::LEFT, *this);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SetChild(direction_in_parent, successor);
|
||||||
|
} else {
|
||||||
|
/* if there is exactly one child, it must be red */
|
||||||
|
assert(GetAnyChild() == nullptr || GetAnyChild()->color == Color::RED);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(!HasTwoChildren());
|
||||||
|
|
||||||
|
auto &p = *parent;
|
||||||
|
|
||||||
|
if (auto *child = GetAnyChild()) {
|
||||||
|
p.ReplaceChild(*this, *child);
|
||||||
|
child->color = Color::BLACK;
|
||||||
|
} else if (IsRoot()) {
|
||||||
|
p.SetChild(Direction::LEFT, nullptr);
|
||||||
|
} else {
|
||||||
|
if (color == Color::BLACK)
|
||||||
|
FixDoubleBlack();
|
||||||
|
|
||||||
|
p.ReplaceChild(*this, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
constexpr std::pair<Direction, RedBlackTreeNode *> GetRedChild() const noexcept {
|
||||||
|
if (auto *left = GetLeft(); left != nullptr && left->color == Color::RED)
|
||||||
|
return {Direction::LEFT, left};
|
||||||
|
|
||||||
|
if (auto *right = GetRight(); right != nullptr && right->color == Color::RED)
|
||||||
|
return {Direction::RIGHT, right};
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void FixDoubleBlack() noexcept {
|
||||||
|
assert(parent != nullptr);
|
||||||
|
assert(!IsHead());
|
||||||
|
assert(color == Color::BLACK);
|
||||||
|
|
||||||
|
if (IsRoot())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto &p = *parent;
|
||||||
|
const auto direction = p.GetChildDirection(*this);
|
||||||
|
const auto other_direction = OtherDirection(direction);
|
||||||
|
auto *const sibling = p.GetChild(other_direction);
|
||||||
|
|
||||||
|
if (sibling == nullptr) {
|
||||||
|
p.FixDoubleBlack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sibling->color) {
|
||||||
|
case Color::RED:
|
||||||
|
p.color = Color::RED;
|
||||||
|
sibling->color = Color::BLACK;
|
||||||
|
|
||||||
|
p.RotateInParent(direction);
|
||||||
|
FixDoubleBlack();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Color::BLACK:
|
||||||
|
if (const auto [red_direction, red] = sibling->GetRedChild(); red != nullptr) {
|
||||||
|
/* at least one red child */
|
||||||
|
|
||||||
|
if (direction == red_direction) {
|
||||||
|
red->color = p.color;
|
||||||
|
sibling->RotateInParent(other_direction);
|
||||||
|
} else {
|
||||||
|
red->color = sibling->color;
|
||||||
|
sibling->color = p.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.RotateInParent(direction);
|
||||||
|
p.color = Color::BLACK;
|
||||||
|
} else {
|
||||||
|
/* no red child (both children are
|
||||||
|
either black or nullptr) */
|
||||||
|
|
||||||
|
sibling->color = Color::RED;
|
||||||
|
if (p.color == Color::BLACK)
|
||||||
|
p.FixDoubleBlack();
|
||||||
|
else
|
||||||
|
p.color = Color::BLACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Color::HEAD:
|
||||||
|
// unreachable
|
||||||
|
assert(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
190
test/util/TestIntrusiveTreeSet.cxx
Normal file
190
test/util/TestIntrusiveTreeSet.cxx
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
// SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
// Copyright CM4all GmbH
|
||||||
|
// author: Max Kellermann <mk@cm4all.com>
|
||||||
|
|
||||||
|
#include "util/IntrusiveTreeSet.hxx"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct IntItem final : IntrusiveTreeSetHook<IntrusiveHookMode::TRACK> {
|
||||||
|
int value;
|
||||||
|
|
||||||
|
IntItem(int _value) noexcept:value(_value) {}
|
||||||
|
|
||||||
|
struct GetKey {
|
||||||
|
constexpr int operator()(const IntItem &item) const noexcept {
|
||||||
|
return item.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
TEST(IntrusiveTreeSet, Basic)
|
||||||
|
{
|
||||||
|
IntItem a{1}, b{2}, c{3}, d{4}, e{5}, f{1};
|
||||||
|
|
||||||
|
IntrusiveTreeSet<IntItem,
|
||||||
|
IntrusiveTreeSetOperators<IntItem, IntItem::GetKey>> set;
|
||||||
|
|
||||||
|
EXPECT_EQ(set.size(), 0U);
|
||||||
|
EXPECT_TRUE(set.empty());
|
||||||
|
|
||||||
|
EXPECT_FALSE(a.is_linked());
|
||||||
|
EXPECT_FALSE(b.is_linked());
|
||||||
|
|
||||||
|
set.insert(b);
|
||||||
|
|
||||||
|
EXPECT_FALSE(a.is_linked());
|
||||||
|
EXPECT_TRUE(b.is_linked());
|
||||||
|
|
||||||
|
EXPECT_EQ(set.size(), 1U);
|
||||||
|
EXPECT_EQ(set.find(2), set.iterator_to(b));
|
||||||
|
EXPECT_EQ(&set.front(), &b);
|
||||||
|
|
||||||
|
set.insert(a);
|
||||||
|
EXPECT_EQ(&set.front(), &a);
|
||||||
|
|
||||||
|
EXPECT_TRUE(a.is_linked());
|
||||||
|
EXPECT_TRUE(b.is_linked());
|
||||||
|
|
||||||
|
set.insert(c);
|
||||||
|
EXPECT_EQ(&set.front(), &a);
|
||||||
|
|
||||||
|
EXPECT_EQ(set.size(), 3U);
|
||||||
|
|
||||||
|
EXPECT_NE(set.find(3), set.end());
|
||||||
|
EXPECT_EQ(set.find(3), set.iterator_to(c));
|
||||||
|
|
||||||
|
EXPECT_EQ(set.find(4), set.end());
|
||||||
|
|
||||||
|
EXPECT_TRUE(c.is_linked());
|
||||||
|
|
||||||
|
set.erase(set.iterator_to(c));
|
||||||
|
|
||||||
|
EXPECT_FALSE(c.is_linked());
|
||||||
|
|
||||||
|
EXPECT_EQ(set.size(), 2U);
|
||||||
|
EXPECT_EQ(set.find(3), set.end());
|
||||||
|
EXPECT_EQ(&set.front(), &a);
|
||||||
|
|
||||||
|
set.insert(c);
|
||||||
|
set.insert(d);
|
||||||
|
set.insert(e);
|
||||||
|
|
||||||
|
EXPECT_EQ(set.size(), 5U);
|
||||||
|
EXPECT_EQ(&set.front(), &a);
|
||||||
|
|
||||||
|
EXPECT_EQ(set.find(1), set.iterator_to(a));
|
||||||
|
EXPECT_EQ(set.find(2), set.iterator_to(b));
|
||||||
|
EXPECT_EQ(set.find(3), set.iterator_to(c));
|
||||||
|
EXPECT_EQ(set.find(4), set.iterator_to(d));
|
||||||
|
EXPECT_EQ(set.find(5), set.iterator_to(e));
|
||||||
|
|
||||||
|
EXPECT_TRUE(a.is_linked());
|
||||||
|
EXPECT_FALSE(f.is_linked());
|
||||||
|
|
||||||
|
set.erase(set.iterator_to(a));
|
||||||
|
EXPECT_FALSE(a.is_linked());
|
||||||
|
EXPECT_FALSE(f.is_linked());
|
||||||
|
EXPECT_EQ(set.find(1), set.end());
|
||||||
|
EXPECT_EQ(set.size(), 4U);
|
||||||
|
EXPECT_EQ(&set.front(), &b);
|
||||||
|
|
||||||
|
set.insert(f);
|
||||||
|
EXPECT_FALSE(a.is_linked());
|
||||||
|
EXPECT_TRUE(f.is_linked());
|
||||||
|
EXPECT_EQ(set.find(1), set.iterator_to(f));
|
||||||
|
EXPECT_EQ(set.size(), 5U);
|
||||||
|
EXPECT_EQ(&set.front(), &f);
|
||||||
|
|
||||||
|
set.pop_front();
|
||||||
|
EXPECT_FALSE(f.is_linked());
|
||||||
|
|
||||||
|
set.clear_and_dispose([](auto *i){ i->value = -1; });
|
||||||
|
|
||||||
|
EXPECT_EQ(set.size(), 0U);
|
||||||
|
EXPECT_TRUE(set.empty());
|
||||||
|
|
||||||
|
EXPECT_EQ(a.value, 1);
|
||||||
|
EXPECT_EQ(b.value, -1);
|
||||||
|
EXPECT_EQ(c.value, -1);
|
||||||
|
EXPECT_EQ(d.value, -1);
|
||||||
|
EXPECT_EQ(e.value, -1);
|
||||||
|
EXPECT_EQ(f.value, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<int... values>
|
||||||
|
static constexpr auto
|
||||||
|
MakeIntItems(std::integer_sequence<int, values...>) noexcept
|
||||||
|
-> std::array<IntItem, sizeof...(values)>
|
||||||
|
{
|
||||||
|
return {values...};
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(IntrusiveTreeSet, RandomOrder)
|
||||||
|
{
|
||||||
|
auto items = MakeIntItems(std::make_integer_sequence<int, 32>());
|
||||||
|
|
||||||
|
IntrusiveTreeSet<IntItem,
|
||||||
|
IntrusiveTreeSetOperators<IntItem, IntItem::GetKey>> set;
|
||||||
|
|
||||||
|
set.insert(items[0]);
|
||||||
|
set.insert(items[5]);
|
||||||
|
set.insert(items[10]);
|
||||||
|
set.insert(items[15]);
|
||||||
|
set.insert(items[20]);
|
||||||
|
set.insert(items[25]);
|
||||||
|
set.insert(items[30]);
|
||||||
|
set.insert(items[1]);
|
||||||
|
set.insert(items[2]);
|
||||||
|
set.insert(items[3]);
|
||||||
|
set.insert(items[31]);
|
||||||
|
set.insert(items[4]);
|
||||||
|
set.insert(items[6]);
|
||||||
|
set.insert(items[7]);
|
||||||
|
set.insert(items[21]);
|
||||||
|
set.insert(items[22]);
|
||||||
|
set.insert(items[23]);
|
||||||
|
set.insert(items[24]);
|
||||||
|
set.insert(items[26]);
|
||||||
|
set.insert(items[8]);
|
||||||
|
set.insert(items[9]);
|
||||||
|
set.insert(items[11]);
|
||||||
|
set.insert(items[12]);
|
||||||
|
set.insert(items[13]);
|
||||||
|
set.insert(items[14]);
|
||||||
|
set.insert(items[27]);
|
||||||
|
set.insert(items[28]);
|
||||||
|
set.insert(items[29]);
|
||||||
|
set.insert(items[16]);
|
||||||
|
set.insert(items[17]);
|
||||||
|
set.insert(items[18]);
|
||||||
|
set.insert(items[19]);
|
||||||
|
|
||||||
|
EXPECT_EQ(set.size(), items.size());
|
||||||
|
|
||||||
|
for (const auto &i : items) {
|
||||||
|
EXPECT_TRUE(i.is_linked());
|
||||||
|
}
|
||||||
|
|
||||||
|
int expected = 0;
|
||||||
|
for (const auto &i : set) {
|
||||||
|
EXPECT_EQ(i.value, expected++);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t remove = 0; remove < items.size(); ++remove) {
|
||||||
|
EXPECT_TRUE(items[remove].is_linked());
|
||||||
|
set.pop_front();
|
||||||
|
EXPECT_FALSE(items[remove].is_linked());
|
||||||
|
|
||||||
|
expected = remove + 1;
|
||||||
|
for (const auto &i : set) {
|
||||||
|
EXPECT_EQ(i.value, expected++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ test(
|
|||||||
'TestIntrusiveForwardList.cxx',
|
'TestIntrusiveForwardList.cxx',
|
||||||
'TestIntrusiveHashSet.cxx',
|
'TestIntrusiveHashSet.cxx',
|
||||||
'TestIntrusiveList.cxx',
|
'TestIntrusiveList.cxx',
|
||||||
|
'TestIntrusiveTreeSet.cxx',
|
||||||
'TestMimeType.cxx',
|
'TestMimeType.cxx',
|
||||||
'TestRingBuffer.cxx',
|
'TestRingBuffer.cxx',
|
||||||
'TestSplitString.cxx',
|
'TestSplitString.cxx',
|
||||||
|
Loading…
Reference in New Issue
Block a user