From fdd5d5fc63ab49d9bc01b3c8cfa8c5e0df8e72ac Mon Sep 17 00:00:00 2001 From: cloudnative0x0 <> Date: Fri, 14 Aug 2026 00:30:37 +0300 Subject: [PATCH] new structure added. --- linked_list/LinkedList.hpp | 541 +++++++++++++++++++++++++++++++ linked_list/README.md | 300 ++++++++++++++++++ linked_list/stress_test.cpp | 616 ++++++++++++++++++++++++++++++++++++ 3 files changed, 1457 insertions(+) create mode 100644 linked_list/LinkedList.hpp create mode 100644 linked_list/README.md create mode 100644 linked_list/stress_test.cpp diff --git a/linked_list/LinkedList.hpp b/linked_list/LinkedList.hpp new file mode 100644 index 0000000..7242895 --- /dev/null +++ b/linked_list/LinkedList.hpp @@ -0,0 +1,541 @@ +#ifndef CP_DATA_STRUCTURES_LINKEDLIST_HPP +#define CP_DATA_STRUCTURES_LINKEDLIST_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cp { + +template +class LinkedList { +public: + using value_type = T; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + +private: + struct NodeBase { + NodeBase* prev; + NodeBase* next; + }; + + struct Node : NodeBase { + T value; + template + explicit Node(Args&&... args) + : NodeBase{nullptr, nullptr}, value(std::forward(args)...) {} + }; + + static Node* asNode(NodeBase* b) noexcept { return static_cast(b); } + static const Node* asNode(const NodeBase* b) noexcept { return static_cast(b); } + + class NodePool { + static constexpr std::size_t kChunkNodes = 1024; + + struct Chunk { + alignas(Node) unsigned char storage[kChunkNodes * sizeof(Node)]; + Chunk* next; + }; + + Chunk* chunks_ = nullptr; + std::size_t used_ = kChunkNodes; + NodeBase* freeList_ = nullptr; + + void* rawAlloc() { + if (freeList_) { + NodeBase* slot = freeList_; + freeList_ = freeList_->next; + return slot; + } + if (used_ == kChunkNodes) { + Chunk* fresh = new Chunk(); + fresh->next = chunks_; + chunks_ = fresh; + used_ = 0; + } + void* slot = chunks_->storage + used_ * sizeof(Node); + ++used_; + + return slot; + } + + public: + NodePool() = default; + NodePool(const NodePool&) = delete; + NodePool& operator=(const NodePool&) = delete; + + ~NodePool() { + Chunk* c = chunks_; + while (c) { + Chunk* nxt = c->next; + delete c; + c = nxt; + } + } + + template + Node* create(Args&&... args) { + void* raw = rawAlloc(); + return ::new (raw) Node(std::forward(args)...); + } + + void destroy(Node* n) noexcept { + n->~Node(); + NodeBase* base = n; + base->next = freeList_; + freeList_ = base; + } + }; + + // Common pool for each instance type T + static NodePool& pool() { + static NodePool instance; + return instance; + } + + NodeBase sentinel_{}; + size_type size_ = 0; + + void initSentinel() noexcept { + sentinel_.prev = &sentinel_; + sentinel_.next = &sentinel_; + } + + static void linkBefore(NodeBase* pos, NodeBase* node) noexcept { + node->prev = pos->prev; + node->next = pos; + pos->prev->next = node; + pos->prev = node; + } + + static void unlink(NodeBase* node) noexcept { + node->prev->next = node->next; + node->next->prev = node->prev; + } + + void stealFrom(LinkedList& other) noexcept { + if (other.empty()) return; + sentinel_.next = other.sentinel_.next; + sentinel_.prev = other.sentinel_.prev; + sentinel_.next->prev = &sentinel_; + sentinel_.prev->next = &sentinel_; + size_ = other.size_; + other.initSentinel(); + other.size_ = 0; + } + +public: + // Iterators — bidirectional + class const_iterator; + + class iterator { + friend class LinkedList; + friend class const_iterator; + NodeBase* node_; + explicit iterator(NodeBase* n) noexcept : node_(n) {} + + public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = T*; + using reference = T&; + + iterator() noexcept : node_(nullptr) {} + + reference operator*() const { return asNode(node_)->value; } + pointer operator->() const { return std::addressof(asNode(node_)->value); } + + iterator& operator++() { node_ = node_->next; return *this; } + iterator operator++(int) { iterator tmp(*this); ++(*this); return tmp; } + iterator& operator--() { node_ = node_->prev; return *this; } + iterator operator--(int) { iterator tmp(*this); --(*this); return tmp; } + + bool operator==(const iterator& o) const noexcept { return node_ == o.node_; } + bool operator!=(const iterator& o) const noexcept { return node_ != o.node_; } + }; + + class const_iterator { + friend class LinkedList; + const NodeBase* node_; + explicit const_iterator(const NodeBase* n) noexcept : node_(n) {} + + public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = const T*; + using reference = const T&; + + const_iterator() noexcept : node_(nullptr) {} + const_iterator(iterator it) noexcept : node_(it.node_) {} + + reference operator*() const { return asNode(node_)->value; } + pointer operator->() const { return std::addressof(asNode(node_)->value); } + + const_iterator& operator++() { node_ = node_->next; return *this; } + const_iterator operator++(int) { const_iterator tmp(*this); ++(*this); return tmp; } + const_iterator& operator--() { node_ = node_->prev; return *this; } + const_iterator operator--(int) { const_iterator tmp(*this); --(*this); return tmp; } + + bool operator==(const const_iterator& o) const noexcept { return node_ == o.node_; } + bool operator!=(const const_iterator& o) const noexcept { return node_ != o.node_; } + }; + + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + // Constructors + LinkedList() noexcept { initSentinel(); } + + LinkedList(std::initializer_list init) : LinkedList() { + for (const auto& v : init) push_back(v); + } + + explicit LinkedList(size_type n, const T& value = T()) : LinkedList() { + for (size_type i = 0; i < n; ++i) push_back(value); + } + + template >> + LinkedList(InputIt first, InputIt last) : LinkedList() { + for (; first != last; ++first) push_back(*first); + } + + LinkedList(const LinkedList& other) : LinkedList() { + for (const auto& v : other) push_back(v); + } + + LinkedList(LinkedList&& other) noexcept : LinkedList() { + stealFrom(other); + } + + LinkedList& operator=(const LinkedList& other) { + if (this != &other) { + LinkedList tmp(other); + swap(tmp); + } + + return *this; + } + + LinkedList& operator=(LinkedList&& other) noexcept { + if (this != &other) { + clear(); + stealFrom(other); + } + + return *this; + } + + LinkedList& operator=(std::initializer_list init) { + LinkedList tmp(init); + swap(tmp); + + return *this; + } + + ~LinkedList() { clear(); } + + // Interation + iterator begin() noexcept { return iterator(sentinel_.next); } + iterator end() noexcept { return iterator(&sentinel_); } + const_iterator begin() const noexcept { return const_iterator(sentinel_.next); } + const_iterator end() const noexcept { return const_iterator(&sentinel_); } + const_iterator cbegin() const noexcept { return begin(); } + const_iterator cend() const noexcept { return end(); } + + reverse_iterator rbegin() noexcept { return reverse_iterator(end()); } + reverse_iterator rend() noexcept { return reverse_iterator(begin()); } + const_reverse_iterator rbegin() const noexcept { return const_reverse_iterator(end()); } + const_reverse_iterator rend() const noexcept { return const_reverse_iterator(begin()); } + const_reverse_iterator crbegin() const noexcept { return rbegin(); } + const_reverse_iterator crend() const noexcept { return rend(); } + + // Capacity + [[nodiscard]] bool empty() const noexcept { return size_ == 0; } + size_type size() const noexcept { return size_; } + size_type max_size() const noexcept { return std::numeric_limits::max(); } + + // Access to the elements + reference front() { return asNode(sentinel_.next)->value; } + const_reference front() const { return asNode(sentinel_.next)->value; } + reference back() { return asNode(sentinel_.prev)->value; } + const_reference back() const { return asNode(sentinel_.prev)->value; } + + // Modificators + void clear() noexcept { + NodeBase* cur = sentinel_.next; + while (cur != &sentinel_) { + NodeBase* nxt = cur->next; + pool().destroy(asNode(cur)); + cur = nxt; + } + initSentinel(); + size_ = 0; + } + + template + iterator emplace(const_iterator pos, Args&&... args) { + Node* node = pool().create(std::forward(args)...); + linkBefore(const_cast(pos.node_), node); + ++size_; + + return iterator(node); + } + + iterator insert(const_iterator pos, const T& value) { return emplace(pos, value); } + iterator insert(const_iterator pos, T&& value) { return emplace(pos, std::move(value)); } + + iterator insert(const_iterator pos, size_type n, const T& value) { + iterator first = iterator(const_cast(pos.node_)); + bool firstSet = false; + for (size_type i = 0; i < n; ++i) { + iterator it = emplace(pos, value); + if (!firstSet) { first = it; firstSet = true; } + } + + return first; + } + + template >> + iterator insert(const_iterator pos, InputIt first, InputIt last) { + iterator result = iterator(const_cast(pos.node_)); + bool firstSet = false; + for (; first != last; ++first) { + iterator it = emplace(pos, *first); + if (!firstSet) { result = it; firstSet = true; } + } + + return result; + } + + iterator erase(const_iterator pos) { + NodeBase* n = const_cast(pos.node_); + NodeBase* nxt = n->next; + unlink(n); + pool().destroy(asNode(n)); + --size_; + + return iterator(nxt); + } + + iterator erase(const_iterator first, const_iterator last) { + while (first != last) first = erase(first); + + return iterator(const_cast(last.node_)); + } + + void push_back(const T& value) { emplace(end(), value); } + void push_back(T&& value) { emplace(end(), std::move(value)); } + template + reference emplace_back(Args&&... args) { return *emplace(end(), std::forward(args)...); } + void pop_back() { erase(iterator(sentinel_.prev)); } + + void push_front(const T& value) { emplace(begin(), value); } + void push_front(T&& value) { emplace(begin(), std::move(value)); } + template + reference emplace_front(Args&&... args) { return *emplace(begin(), std::forward(args)...); } + void pop_front() { erase(begin()); } + + void resize(size_type count) { resize(count, T()); } + void resize(size_type count, const T& value) { + if (count < size_) { while (size_ > count) pop_back(); } + else { while (size_ < count) push_back(value); } + } + + void swap(LinkedList& other) noexcept { + if (this == &other) return; + + NodeBase* aNext = sentinel_.next; + NodeBase* aPrev = sentinel_.prev; + NodeBase* bNext = other.sentinel_.next; + NodeBase* bPrev = other.sentinel_.prev; + const bool aEmpty = empty(); + const bool bEmpty = other.empty(); + + if (aEmpty) { + other.sentinel_.next = &other.sentinel_; + other.sentinel_.prev = &other.sentinel_; + } else { + other.sentinel_.next = aNext; + other.sentinel_.prev = aPrev; + aNext->prev = &other.sentinel_; + aPrev->next = &other.sentinel_; + } + + if (bEmpty) { + sentinel_.next = &sentinel_; + sentinel_.prev = &sentinel_; + } else { + sentinel_.next = bNext; + sentinel_.prev = bPrev; + bNext->prev = &sentinel_; + bPrev->next = &sentinel_; + } + + std::swap(size_, other.size_); + } + + // splice — honest O(1) + void splice(const_iterator pos, LinkedList& other) noexcept { + splice(pos, other, other.begin(), other.end()); + } + + void splice(const_iterator pos, LinkedList& other, const_iterator it) noexcept { + const_iterator next = it; ++next; + splice(pos, other, it, next); + } + + void splice(const_iterator pos, LinkedList& other, + const_iterator first, const_iterator last) noexcept { + if (first == last) return; + + NodeBase* posN = const_cast(pos.node_); + NodeBase* firstN = const_cast(first.node_); + NodeBase* lastExclN = const_cast(last.node_); + NodeBase* lastInclN = lastExclN->prev; + + if (&other == this) { + for (NodeBase* p = firstN; p != lastExclN; p = p->next) { + if (posN == p) return; + } + } + + size_type moved = 0; + if (&other != this) { + for (NodeBase* p = firstN; p != lastExclN; p = p->next) ++moved; + } + + firstN->prev->next = lastExclN; + lastExclN->prev = firstN->prev; + + NodeBase* before = posN->prev; + before->next = firstN; + firstN->prev = before; + lastInclN->next = posN; + posN->prev = lastInclN; + + if (&other != this) { + size_ += moved; + other.size_ -= moved; + } + } + + // Algorithms + void reverse() noexcept { + NodeBase* cur = &sentinel_; + do { + NodeBase* nxt = cur->next; + cur->next = cur->prev; + cur->prev = nxt; + cur = nxt; + } while (cur != &sentinel_); + } + + template + size_type remove_if(UnaryPred pred) { + size_type removed = 0; + for (iterator it = begin(); it != end(); ) { + if (pred(*it)) { it = erase(it); ++removed; } + else { ++it; } + } + + return removed; + } + + size_type remove(const T& value) { + return remove_if([&value](const T& v) { return v == value; }); + } + + template > + size_type unique(BinaryPred pred = BinaryPred()) { + if (size_ < 2) return 0; + size_type removed = 0; + iterator it = begin(); + iterator nxt = it; ++nxt; + + while (nxt != end()) { + if (pred(*it, *nxt)) { nxt = erase(nxt); ++removed; } + else { it = nxt; ++nxt; } + } + + return removed; + } + + template > + void sort(Compare comp = Compare()) { + if (size_ < 2) return; + sentinel_.prev->next = nullptr; + NodeBase* head = sentinel_.next; + head = mergeSort(head, comp); + + NodeBase* prev = &sentinel_; + NodeBase* cur = head; + while (cur) { + prev->next = cur; + cur->prev = prev; + prev = cur; + cur = cur->next; + } + prev->next = &sentinel_; + sentinel_.prev = prev; + } + + friend bool operator==(const LinkedList& a, const LinkedList& b) { + if (a.size_ != b.size_) return false; + + return std::equal(a.begin(), a.end(), b.begin()); + } + friend bool operator!=(const LinkedList& a, const LinkedList& b) { return !(a == b); } + +private: + template + static NodeBase* mergeSort(NodeBase* head, Compare& comp) { + if (!head || !head->next) return head; + + NodeBase* slow = head; + NodeBase* fast = head->next; + while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } + NodeBase* rightHead = slow->next; + slow->next = nullptr; + + NodeBase* left = mergeSort(head, comp); + NodeBase* right = mergeSort(rightHead, comp); + + return merge(left, right, comp); + } + + template + static NodeBase* merge(NodeBase* a, NodeBase* b, Compare& comp) { + NodeBase dummy{nullptr, nullptr}; + NodeBase* tail = &dummy; + while (a && b) { + if (comp(asNode(b)->value, asNode(a)->value)) { tail->next = b; b = b->next; } + else { tail->next = a; a = a->next; } + tail = tail->next; + } + tail->next = a ? a : b; + + return dummy.next; + } +}; + +template +void swap(LinkedList& a, LinkedList& b) noexcept { a.swap(b); } + +} + +#endif // CP_DATA_STRUCTURES_LINKEDLIST_HPP \ No newline at end of file diff --git a/linked_list/README.md b/linked_list/README.md new file mode 100644 index 0000000..e6f31c6 --- /dev/null +++ b/linked_list/README.md @@ -0,0 +1,300 @@ +# LinkedList + +

+ РусскийEnglish +

+ +--- + +## Русский + +Список — это цепочка узлов. Каждый узел хранит значение и два указателя: на предыдущий узел и на следующий. Никакого массива внутри нет, элементы не лежат подряд в памяти — они разбросаны как попало, а порядок задаётся именно этими указателями. + +Из этого вытекают два свойства, о которых стоит помнить, когда выбираешь список вместо массива или вектора: + +```cpp +#include "LinkedList.hpp" + +cp::LinkedList list = {1, 2, 3}; +``` + +- **Вставить или удалить элемент где угодно — дёшево.** Если у вас уже есть итератор на нужное место, вставка или удаление — это переставить пару указателей, O(1), независимо от того, сколько всего элементов в списке. +- **Обратиться к элементу по номеру — дорого.** Нет никакого `list[50]`. Чтобы дойти до пятидесятого элемента, нужно пройти через предыдущие сорок девять один за другим, O(n). + +Если вам нужен быстрый доступ по индексу — берите `std::vector`. Если нужно много вставлять и удалять в середине, особенно большие объекты, где копирование дорого, — список подходит лучше. + +### Как это устроено внутри + +#### Кольцо с "часовым" узлом + +Обычно список хранят как `head` и `tail`, и почти в каждой операции приходится отдельно обрабатывать случай пустого списка, случай вставки в начало и случай вставки в конец. Здесь это устроено иначе: список замкнут в кольцо, и в этом кольце есть один служебный узел — `sentinel_` — который не хранит значения, а просто обозначает границу между концом списка и его началом. + +``` +sentinel_ → [1] → [2] → [3] → sentinel_ → [1] → ... +``` + +`begin()` — это первый настоящий элемент, `end()` — это сам `sentinel_`. Когда список пуст, `sentinel_` указывает сам на себя, и `begin() == end()` получается автоматически, без единой проверки "а пуст ли список". + +Благодаря этому трюку вставка перед первым элементом, вставка перед последним и вставка в середину — буквально один и тот же код. Никаких особых случаев для головы и хвоста. + +#### Память под узлы берётся не через new/delete напрямую + +Каждый раз выделять и освобождать память под один узел через `new`/`delete` — довольно медленно. Вместо этого используется пул: память выделяется сразу большими кусками по 1024 узла, и когда узел удаляется, его память не возвращается системе, а просто помечается как свободная и переиспользуется под следующий новый узел. + +Практический смысл: если вы часто вставляете и удаляете элементы, список не будет постоянно дёргать аллокатор — переиспользование памяти происходит внутри пула, быстро. + +Из этого следует один нюанс: память, которую список один раз занял, не возвращается операционной системе, пока программа не завершится. Для большинства задач это не проблема, но если вы создаёте и уничтожаете очень много списков с очень большими объектами — стоит иметь это в виду. + +### Как пользоваться + +#### Создание + +```cpp +cp::LinkedList a; // пустой список +cp::LinkedList b = {1, 2, 3, 4}; // из списка значений +cp::LinkedList c(5, 0); // пять нулей +cp::LinkedList d(b.begin(), b.end()); // копия диапазона +cp::LinkedList e = b; // копия +cp::LinkedList f = std::move(b); // перемещение, b становится пустым +``` + +#### Добавление и удаление элементов + +```cpp +cp::LinkedList list = {2, 3, 4}; + +list.push_back(5); // 2 3 4 5 +list.push_front(1); // 1 2 3 4 5 +list.pop_back(); // 1 2 3 4 +list.pop_front(); // 2 3 4 + +auto it = list.begin(); +++it; // указывает на второй элемент +list.insert(it, 100); // вставить перед ним +list.erase(it); // удалить сам этот элемент +``` + +`push_back`/`push_front` добавляют в конец/начало. `insert(pos, value)` вставляет перед позицией `pos`. `erase(pos)` удаляет элемент в этой позиции. Всё это работает за O(1), если позиция у вас уже есть в виде итератора. + +#### Обход + +```cpp +for (int x : list) { + // обычный проход от начала до конца +} + +for (auto it = list.rbegin(); it != list.rend(); ++it) { + // проход в обратном порядке +} +``` + +#### Доступ к краям + +```cpp +list.front(); // первый элемент +list.back(); // последний элемент +list.empty(); // пуст ли список +list.size(); // сколько элементов +``` + +Обратите внимание: `front()`/`back()` ничего не проверяют сами — если вызвать их на пустом списке, это ошибка на совести вызывающего кода, точно как и в `std::list`. + +#### Полезные алгоритмы уже встроены + +```cpp +list.sort(); // отсортировать +list.reverse(); // развернуть список +list.unique(); // убрать соседние дубликаты +list.remove(3); // убрать все элементы, равные 3 +list.remove_if([](int x) { return x < 0; }); // убрать все отрицательные +``` + +`sort()` сортирует за O(n log n) и не выделяет память под сами элементы — переставляются узлы, а не копируются значения. Это особенно выгодно, если `T` — что-то тяжёлое для копирования. + +#### Перенос кусков между списками — splice + +```cpp +cp::LinkedList a = {1, 2, 3}; +cp::LinkedList b = {100, 200}; + +a.splice(a.begin(), b); // весь b переезжает в начало a, b становится пустым +// a: 100 200 1 2 3 +``` + +`splice` переносит элементы из одного списка в другой без копирования — просто перевешивает указатели. Можно перенести весь список, один элемент или диапазон: + +```cpp +a.splice(pos, other); // весь список other +a.splice(pos, other, other_it); // один элемент +a.splice(pos, other, first, last); // диапазон [first, last) +``` + +### Таблица операций + +| Операция | Что делает | Сложность | +|---|---|---| +| `push_back` / `push_front` | добавить в конец / начало | O(1) | +| `pop_back` / `pop_front` | убрать с конца / начала | O(1) | +| `insert(pos, value)` | вставить перед позицией | O(1) | +| `erase(pos)` | удалить элемент | O(1) | +| `front()` / `back()` | первый / последний элемент | O(1) | +| `size()` / `empty()` | размер / пустота | O(1) | +| доступ по индексу | — | не поддерживается вообще | +| `splice` | перенести кусок из другого списка | O(1) (кроме подсчёта размера при переносе между разными списками) | +| `sort()` | отсортировать | O(n log n) | +| `reverse()` | развернуть | O(n) | +| `remove` / `remove_if` / `unique` | удалить по условию | O(n) | + +### Когда это не подходит + +- Нужен быстрый доступ по индексу — берите `std::vector`. +- Элементы маленькие (например, `int`), а список короткий — накладные расходы на указатели (два указателя на каждый `int`) и на непоследовательное расположение в памяти (кэш-промахи при обходе) часто делают связный список медленнее вектора даже там, где по теории он должен выигрывать. +- Нужен доступ из нескольких потоков одновременно — список это не поддерживает, синхронизацию нужно делать снаружи. + +--- + +## English + +A list is a chain of nodes. Each node stores a value and two pointers: one to the previous node, one to the next. There's no array underneath — elements don't sit next to each other in memory, they're scattered wherever, and order is defined purely by those pointers. + +Two properties follow from this, worth keeping in mind when choosing a list over an array or a vector: + + +```cpp +#include "LinkedList.hpp" + +cp::LinkedList list = {1, 2, 3}; +``` + +- **Inserting or removing anywhere is cheap.** If you already have an iterator to the right spot, insertion or removal is just relinking a couple of pointers — O(1), regardless of how many elements the list holds. +- **Reaching an element by its position is expensive.** There's no `list[50]`. To get to the fiftieth element you have to walk through the previous forty-nine, one by one — O(n). + +If you need fast index-based access, use `std::vector`. If you're inserting and removing a lot in the middle, especially with objects that are expensive to copy, a list is the better fit. + +### How it works internally + +#### A ring with a sentinel node + +Lists are usually stored as `head` and `tail`, and nearly every operation ends up handling the empty-list case, the insert-at-front case, and the insert-at-back case separately. Here it works differently: the list is closed into a ring, and inside that ring sits one housekeeping node — `sentinel_` — which doesn't hold a value, it just marks the boundary between the end of the list and its start. + +``` +sentinel_ → [1] → [2] → [3] → sentinel_ → [1] → ... +``` + +`begin()` is the first real element, `end()` is `sentinel_` itself. When the list is empty, `sentinel_` points at itself, and `begin() == end()` follows automatically, with no "is the list empty" check anywhere. + +Because of this, inserting before the first element, before the last one, and in the middle are literally the same code path. No special cases for head or tail. + +#### Node memory doesn't come straight from new/delete + +Allocating and freeing memory for a single node through `new`/`delete` every time is fairly slow. Instead a pool is used: memory is allocated in big chunks of 1024 nodes at once, and when a node is erased its memory isn't handed back to the system — it's just marked as free and reused for the next new node. + +The practical effect: if you insert and remove elements often, the list doesn't keep hitting the allocator — memory gets recycled inside the pool, quickly. + +One consequence follows from this: memory the list has once claimed isn't returned to the operating system until the program ends. For most tasks that's not an issue, but if you're creating and destroying a very large number of lists holding very large objects, it's worth being aware of. + +### How to use it + +#### Creating a list + +```cpp +cp::LinkedList a; // empty list +cp::LinkedList b = {1, 2, 3, 4}; // from a list of values +cp::LinkedList c(5, 0); // five zeros +cp::LinkedList d(b.begin(), b.end()); // copy of a range +cp::LinkedList e = b; // copy +cp::LinkedList f = std::move(b); // move, b becomes empty +``` + +#### Adding and removing elements + +```cpp +cp::LinkedList list = {2, 3, 4}; + +list.push_back(5); // 2 3 4 5 +list.push_front(1); // 1 2 3 4 5 +list.pop_back(); // 1 2 3 4 +list.pop_front(); // 2 3 4 + +auto it = list.begin(); +++it; // points to the second element +list.insert(it, 100); // insert before it +list.erase(it); // remove that element itself +``` + +`push_back`/`push_front` add to the end/front. `insert(pos, value)` inserts before `pos`. `erase(pos)` removes the element at that position. All of this runs in O(1) as long as you already hold the position as an iterator. + +#### Traversal + +```cpp +for (int x : list) { + // regular front-to-back pass +} + +for (auto it = list.rbegin(); it != list.rend(); ++it) { + // reverse pass +} +``` + +#### Accessing the ends + +```cpp +list.front(); // first element +list.back(); // last element +list.empty(); // is the list empty +list.size(); // how many elements +``` + +Note: `front()`/`back()` don't check anything themselves — calling them on an empty list is on the caller, exactly as with `std::list`. + +#### Algorithms already built in + +```cpp +list.sort(); // sort +list.reverse(); // reverse the list +list.unique(); // drop adjacent duplicates +list.remove(3); // remove every element equal to 3 +list.remove_if([](int x) { return x < 0; }); // remove every negative element +``` + +`sort()` runs in O(n log n) and doesn't allocate memory for the elements themselves — nodes get rearranged, values don't get copied. That's especially useful when `T` is expensive to copy. + +#### Moving chunks between lists — splice + +```cpp +cp::LinkedList a = {1, 2, 3}; +cp::LinkedList b = {100, 200}; + +a.splice(a.begin(), b); // all of b moves to the front of a, b becomes empty +// a: 100 200 1 2 3 +``` + +`splice` moves elements from one list into another without copying — it just rewires pointers. You can move the whole list, a single element, or a range: + +```cpp +a.splice(pos, other); // the entire other list +a.splice(pos, other, other_it); // a single element +a.splice(pos, other, first, last); // a range [first, last) +``` + +### Operation table + +| Operation | What it does | Complexity | +|---|---|---| +| `push_back` / `push_front` | add to the end / front | O(1) | +| `pop_back` / `pop_front` | remove from the end / front | O(1) | +| `insert(pos, value)` | insert before a position | O(1) | +| `erase(pos)` | remove an element | O(1) | +| `front()` / `back()` | first / last element | O(1) | +| `size()` / `empty()` | size / emptiness | O(1) | +| index-based access | — | not supported at all | +| `splice` | move a chunk from another list | O(1) (except for the size count when moving between different lists) | +| `sort()` | sort | O(n log n) | +| `reverse()` | reverse | O(n) | +| `remove` / `remove_if` / `unique` | conditional removal | O(n) | + +### When this isn't the right choice + +- You need fast index-based access — use `std::vector`. +- The elements are small (like `int`) and the list is short — the overhead of two pointers per element and the scattered memory layout (cache misses while walking the list) often make a linked list slower than a vector even in cases where it should theoretically win. +- You need concurrent access from multiple threads — the list doesn't support this, synchronization has to be handled outside it. \ No newline at end of file diff --git a/linked_list/stress_test.cpp b/linked_list/stress_test.cpp new file mode 100644 index 0000000..36a1935 --- /dev/null +++ b/linked_list/stress_test.cpp @@ -0,0 +1,616 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "LinkedList.hpp" + +using cp::LinkedList; + +int g_checks = 0; +int g_failures = 0; + +void check(bool cond, const std::string& what) { + ++g_checks; + if (!cond) { + ++g_failures; + std::cerr << "FAILED: " << what << "\n"; + } +} + +void assertSameContents(const LinkedList& a, const std::list& b, + const std::string& ctx) { + check(a.size() == b.size(), ctx + ": size mismatch"); + if (a.size() != b.size()) return; + + if (!b.empty()) { + check(a.front() == b.front(), ctx + ": front mismatch"); + check(a.back() == b.back(), ctx + ": back mismatch"); + } + + { + auto ai = a.begin(); + auto bi = b.begin(); + std::size_t idx = 0; + for (; ai != a.end() && bi != b.end(); ++ai, ++bi, ++idx) { + if (*ai != *bi) { + check(false, ctx + ": forward mismatch at index " + std::to_string(idx)); + break; + } + } + check(ai == a.end(), ctx + ": forward iteration of `a` ran long/short"); + check(bi == b.end(), ctx + ": forward iteration of `b` ran long/short"); + } + + { + auto ai = a.rbegin(); + auto bi = b.rbegin(); + std::size_t idx = 0; + for (; ai != a.rend() && bi != b.rend(); ++ai, ++bi, ++idx) { + if (*ai != *bi) { + check(false, ctx + ": backward mismatch at index " + std::to_string(idx)); + break; + } + } + check(ai == a.rend(), ctx + ": backward iteration of `a` ran long/short"); + check(bi == b.rend(), ctx + ": backward iteration of `b` ran long/short"); + } + + check(static_cast(std::distance(a.begin(), a.end())) == a.size(), + ctx + ": distance(begin,end) != size()"); +} + +template +Iter advanced(Iter it, long k) { + std::advance(it, k); + return it; +} + +void test_basic_push_pop() { + LinkedList l; + check(l.empty(), "l.empty()"); + l.push_back(1); + l.push_back(2); + l.push_front(0); + + std::vector got(l.begin(), l.end()); + check((got == std::vector{0, 1, 2}), "(got == std::vector{0, 1, 2})"); + check(l.front() == 0, "l.front() == 0"); + check(l.back() == 2, "l.back() == 2"); + + l.pop_front(); + l.pop_back(); + check(l.size() == 1, "l.size() == 1"); + check(l.front() == 1, "l.front() == 1"); + l.pop_back(); + check(l.empty(), "l.empty()"); +} + +void test_copy_and_move() { + LinkedList a{1, 2, 3, 4, 5}; + LinkedList b = a; + check(a == b, "a == b"); + b.push_back(6); + check(a != b, "a != b"); + + LinkedList c = std::move(b); + check((c == LinkedList{1, 2, 3, 4, 5, 6}), "(c == LinkedList{1, 2, 3, 4, 5, 6})"); + check(b.empty(), "b.empty()"); + + LinkedList d; + d.push_back(42); + d = a; + check(d == a, "d == a"); + + LinkedList e; + e.push_back(-1); + e = std::move(c); + check((e == LinkedList{1, 2, 3, 4, 5, 6}), "(e == LinkedList{1, 2, 3, 4, 5, 6})"); + check(c.empty(), "c.empty()"); +} + +void test_self_assignment_and_self_swap() { + LinkedList a{1, 2, 3}; + + a = a; + check((a == LinkedList{1, 2, 3}), "(a == LinkedList{1, 2, 3})"); + + a.swap(a); + check((a == LinkedList{1, 2, 3}), "(a == LinkedList{1, 2, 3})"); + + a = std::move(a); + check(a.size() <= 3, "a.size() <= 3"); + + std::size_t cnt = 0; + for (auto it = a.begin(); it != a.end() && cnt <= 10; ++it) ++cnt; + check(cnt == a.size(), "cnt == a.size()"); +} + +void test_erase_and_ranges() { + LinkedList a{1, 2, 3, 4, 5}; + auto it = a.begin(); + ++it; + it = a.erase(it); + check(*it == 3, "*it == 3"); + check((a == LinkedList{1, 3, 4, 5}), "(a == LinkedList{1, 3, 4, 5})"); + + auto first = a.begin(); + auto last = a.end(); + auto res = a.erase(first, last); + check(a.empty(), "a.empty()"); + check(res == a.end(), "res == a.end()"); + + LinkedList b{1, 2, 3}; + auto bit = b.begin(); + ++bit; + auto same = b.erase(bit, bit); + check(*same == 2, "*same == 2"); + check((b == LinkedList{1, 2, 3}), "(b == LinkedList{1, 2, 3})"); +} + +void test_insert_variants() { + LinkedList a{1, 5}; + auto pos = a.begin(); + ++pos; + a.insert(pos, 3); + check((a == LinkedList{1, 3, 5}), "(a == LinkedList{1, 3, 5})"); + + a.insert(a.end(), std::size_t{3}, 9); + check((a == LinkedList{1, 3, 5, 9, 9, 9}), "(a == LinkedList{1, 3, 5, 9, 9, 9})"); + + std::vector src{100, 200, 300}; + a.insert(a.begin(), src.begin(), src.end()); + check((a == LinkedList{100, 200, 300, 1, 3, 5, 9, 9, 9}), "(a == LinkedList{100, 200, 300, 1, 3, 5, 9, 9, 9})"); + + LinkedList b{7, 8}; + auto it = b.begin(); + ++it; + auto r = b.insert(it, std::size_t{0}, 42); + check(*r == 8, "*r == 8"); + check((b == LinkedList{7, 8}), "(b == LinkedList{7, 8})"); +} + +void test_remove_remove_if_unique() { + LinkedList a{1, 2, 2, 3, 3, 3, 4, 1}; + auto removed = a.remove(3); + check(removed == 3, "removed == 3"); + check((a == LinkedList{1, 2, 2, 4, 1}), "(a == LinkedList{1, 2, 2, 4, 1})"); + + auto removed_if = a.remove_if([](int v) { return v % 2 == 0; }); + check(removed_if == 3, "removed_if == 3"); + check((a == LinkedList{1, 1}), "(a == LinkedList{1, 1})"); + + LinkedList b{1, 1, 2, 2, 2, 3, 1, 1}; + auto u = b.unique(); + check(u == 4, "u == 4"); + check((b == LinkedList{1, 2, 3, 1}), "(b == LinkedList{1, 2, 3, 1})"); + + LinkedList empty1; + check(empty1.unique() == 0, "empty1.unique() == 0"); + LinkedList one{5}; + check(one.unique() == 0, "one.unique() == 0"); +} + +void test_sort_stability_and_reverse() { + + struct Pair { + int key; + int orig; + }; + LinkedList l; + std::vector ref; + std::mt19937 rng(12345); + std::uniform_int_distribution keyDist(0, 4); + for (int i = 0; i < 500; ++i) { + Pair p{keyDist(rng), i}; + l.push_back(p); + ref.push_back(p); + } + l.sort([](const Pair& x, const Pair& y) { return x.key < y.key; }); + std::stable_sort(ref.begin(), ref.end(), + [](const Pair& x, const Pair& y) { return x.key < y.key; }); + + std::vector got(l.begin(), l.end()); + check(got.size() == ref.size(), "sort: size mismatch"); + bool same = std::equal(got.begin(), got.end(), ref.begin(), ref.end(), + [](const Pair& x, const Pair& y) { + return x.key == y.key && x.orig == y.orig; + }); + check(same, "merge sort is not stable (or produced wrong order)"); + + LinkedList r{1, 2, 3, 4, 5}; + r.reverse(); + check((r == LinkedList{5, 4, 3, 2, 1}), "(r == LinkedList{5, 4, 3, 2, 1})"); + r.reverse(); + check((r == LinkedList{1, 2, 3, 4, 5}), "(r == LinkedList{1, 2, 3, 4, 5})"); + + LinkedList single{1}; + single.reverse(); + check((single == LinkedList{1}), "(single == LinkedList{1})"); + + LinkedList emptyList; + emptyList.reverse(); + check(emptyList.empty(), "emptyList.empty()"); +} + +void test_splice_whole_and_element_and_range() { + std::cout << " [splice] whole\n" << std::flush; + + { + LinkedList a{1, 2, 3}; + LinkedList b{10, 20}; + auto pos = a.begin(); + ++pos; + a.splice(pos, b); + check((a == LinkedList{1, 10, 20, 2, 3}), "(a == LinkedList{1, 10, 20, 2, 3})"); + check(b.empty(), "b.empty()"); + check(b.size() == 0, "b.size() == 0"); + } + + std::cout << " [splice] single elem cross-list\n" << std::flush; + + { + LinkedList a{1, 2, 3}; + LinkedList b{10, 20, 30}; + auto it = b.begin(); + ++it; + a.splice(a.end(), b, it); + check((a == LinkedList{1, 2, 3, 20}), "(a == LinkedList{1, 2, 3, 20})"); + check((b == LinkedList{10, 30}), "(b == LinkedList{10, 30})"); + } + + std::cout << " [splice] range cross-list\n" << std::flush; + + { + LinkedList a{1, 2}; + LinkedList b{10, 20, 30, 40}; + auto f = b.begin(); + ++f; + auto l = f; + std::advance(l, 2); + a.splice(a.begin(), b, f, l); + check((a == LinkedList{20, 30, 1, 2}), "(a == LinkedList{20, 30, 1, 2})"); + check((b == LinkedList{10, 40}), "(b == LinkedList{10, 40})"); + } + + std::cout << " [splice] same-list range move\n" << std::flush; + + { + LinkedList a{1, 2, 3, 4, 5}; + auto f = a.begin(); + std::advance(f, 1); + auto l = a.begin(); + std::advance(l, 3); + a.splice(a.end(), a, f, l); + check((a == LinkedList{1, 4, 5, 2, 3}), "(a == LinkedList{1, 4, 5, 2, 3})"); + check(a.size() == 5, "a.size() == 5"); + } + + std::cout << " [splice] empty range self\n" << std::flush; + + { + LinkedList a{1, 2, 3}; + auto it = a.begin(); + ++it; + a.splice(a.end(), a, it, it); + check((a == LinkedList{1, 2, 3}), "(a == LinkedList{1, 2, 3})"); + } + + std::cout << " [splice] self-splice pos==it (KNOWN BUG — run isolated/bounded)\n" << std::flush; + { + +#ifdef HAVE_LSAN_INTERFACE + __lsan::ScopedDisabler lsanDisabler; +#endif + auto* a = new LinkedList{1, 2, 3}; + auto it = a->begin(); + ++it; + a->splice(it, *a, it); + + bool sizeOk = (a->size() == 3); + check(sizeOk, "self-splice(pos==it): size() changed (expected no-op)"); + + std::vector got; + auto cur = a->begin(); + std::size_t steps = 0; + constexpr std::size_t kStepLimit = 16; + while (cur != a->end() && steps < kStepLimit) { + got.push_back(*cur); + ++cur; + ++steps; + } + bool structureOk = (steps < kStepLimit) && (got == std::vector{1, 2, 3}); + check(structureOk, + "BUG CONFIRMED: splice(pos, list, it) with pos == it corrupts the " + "list into a detached self-cycle instead of being a no-op " + "(see comment above for the exact pointer-aliasing bug in " + "LinkedList::splice). Do not call splice with a destination " + "iterator that lies inside the moved range, including pos == first."); + + } + + { + LinkedList a{1, 2, 3}; + auto it = a.begin(); + auto pos = it; ++pos; ++pos; + + auto posOn2 = a.begin(); ++posOn2; + a.splice(posOn2, a, it); + check(a.size() == 3, "self-splice(next, it) corrupted size"); + std::vector got(a.begin(), a.end()); + check((got == std::vector{1, 2, 3}), + "self-splice(pos==next(it)) must be a no-op"); + } +} + +void test_resize() { + LinkedList a{1, 2, 3}; + a.resize(5, -1); + check((a == LinkedList{1, 2, 3, -1, -1}), "(a == LinkedList{1, 2, 3, -1, -1})"); + a.resize(2); + check((a == LinkedList{1, 2}), "(a == LinkedList{1, 2})"); + a.resize(0); + check(a.empty(), "a.empty()"); + a.resize(3, 7); + check((a == LinkedList{7, 7, 7}), "(a == LinkedList{7, 7, 7})"); +} + +void test_const_iterator_conversion_and_reverse_iterators() { + LinkedList a{1, 2, 3}; + LinkedList::const_iterator cit = a.begin(); + check(*cit == 1, "*cit == 1"); + + std::vector rev(a.rbegin(), a.rend()); + check((rev == std::vector{3, 2, 1}), "(rev == std::vector{3, 2, 1})"); + + const LinkedList& ca = a; + std::vector crev(ca.crbegin(), ca.crend()); + check((crev == std::vector{3, 2, 1}), "(crev == std::vector{3, 2, 1})"); +} + +struct Model { + LinkedList lst; + std::list ref; + + void check(const std::string& ctx) { assertSameContents(lst, ref, ctx); } +}; + +void fuzz_run(std::uint32_t seed, int iterations) { + std::mt19937 rng(seed); + std::uniform_int_distribution valueDist(-1000, 1000); + + Model A, B; + + auto randPos = [&](std::size_t sz) -> long { + if (sz == 0) return 0; + std::uniform_int_distribution d(0, static_cast(sz)); + return d(rng); + }; + + for (int iter = 0; iter < iterations; ++iter) { + std::uniform_int_distribution opDist(0, 21); + int op = opDist(rng); + Model& M = (rng() % 5 == 0) ? B : A; + + switch (op) { + case 0: { + int v = valueDist(rng); + M.lst.push_back(v); + M.ref.push_back(v); + break; + } + case 1: { + int v = valueDist(rng); + M.lst.push_front(v); + M.ref.push_front(v); + break; + } + case 2: { + if (!M.ref.empty()) { M.lst.pop_back(); M.ref.pop_back(); } + break; + } + case 3: { + if (!M.ref.empty()) { M.lst.pop_front(); M.ref.pop_front(); } + break; + } + case 4: { + long k = randPos(M.ref.size()); + int v = valueDist(rng); + M.lst.insert(advanced(M.lst.begin(), k), v); + M.ref.insert(advanced(M.ref.begin(), k), v); + break; + } + case 5: { + if (!M.ref.empty()) { + long k = randPos(M.ref.size() - 1); + M.lst.erase(advanced(M.lst.begin(), k)); + M.ref.erase(advanced(M.ref.begin(), k)); + } + break; + } + case 6: { + if (!M.ref.empty()) { + long sz = static_cast(M.ref.size()); + long i = randPos(sz - 1); + long j = i + (rng() % (sz - i + 1)); + M.lst.erase(advanced(M.lst.begin(), i), advanced(M.lst.begin(), j)); + M.ref.erase(advanced(M.ref.begin(), i), advanced(M.ref.begin(), j)); + } + break; + } + case 7: { + M.lst.clear(); + M.ref.clear(); + break; + } + case 8: { + std::uniform_int_distribution cd(0, 30); + std::size_t cnt = cd(rng); + int v = valueDist(rng); + M.lst.resize(cnt, v); + M.ref.resize(cnt, v); + break; + } + case 9: { + M.lst.reverse(); + M.ref.reverse(); + break; + } + case 10: { + M.lst.sort(); + M.ref.sort(); + break; + } + case 11: { + M.lst.unique(); + M.ref.unique(); + break; + } + case 12: { + int v = valueDist(rng); + auto r1 = M.lst.remove(v); + + std::size_t before = M.ref.size(); + M.ref.remove(v); + std::size_t r2 = before - M.ref.size(); + check(r1 == r2, "remove() count mismatch"); + break; + } + case 13: { + auto r1 = M.lst.remove_if([](int v) { return v % 3 == 0; }); + std::size_t before = M.ref.size(); + M.ref.remove_if([](int v) { return v % 3 == 0; }); + std::size_t r2 = before - M.ref.size(); + check(r1 == r2, "remove_if() count mismatch"); + break; + } + case 14: { + if (&M == &A && !B.ref.empty()) { + long k = randPos(A.ref.size()); + A.lst.splice(advanced(A.lst.begin(), k), B.lst); + A.ref.splice(advanced(A.ref.begin(), k), B.ref); + } else if (&M == &B && !A.ref.empty()) { + long k = randPos(B.ref.size()); + B.lst.splice(advanced(B.lst.begin(), k), A.lst); + B.ref.splice(advanced(B.ref.begin(), k), A.ref); + } + break; + } + case 15: { + Model& Src = (&M == &A) ? B : A; + if (!Src.ref.empty()) { + long srcK = randPos(Src.ref.size() - 1); + long dstK = randPos(M.ref.size()); + M.lst.splice(advanced(M.lst.begin(), dstK), Src.lst, + advanced(Src.lst.begin(), srcK)); + M.ref.splice(advanced(M.ref.begin(), dstK), Src.ref, + advanced(Src.ref.begin(), srcK)); + } + break; + } + case 16: { + Model& Src = (&M == &A) ? B : A; + if (!Src.ref.empty()) { + long sz = static_cast(Src.ref.size()); + long i = randPos(sz - 1); + long j = i + (rng() % (sz - i + 1)); + long dstK = randPos(M.ref.size()); + M.lst.splice(advanced(M.lst.begin(), dstK), Src.lst, + advanced(Src.lst.begin(), i), advanced(Src.lst.begin(), j)); + M.ref.splice(advanced(M.ref.begin(), dstK), Src.ref, + advanced(Src.ref.begin(), i), advanced(Src.ref.begin(), j)); + } + break; + } + case 17: { + if (M.ref.size() >= 2) { + long sz = static_cast(M.ref.size()); + long i = randPos(sz - 1); + long j = i + (rng() % (sz - i + 1)); + + long dstK; + if (i == 0 && j == sz) break; + if (rng() % 2 == 0 && i > 0) dstK = randPos(i - 1); + else dstK = j + (rng() % (sz - j + 1)); + M.lst.splice(advanced(M.lst.begin(), dstK), M.lst, + advanced(M.lst.begin(), i), advanced(M.lst.begin(), j)); + M.ref.splice(advanced(M.ref.begin(), dstK), M.ref, + advanced(M.ref.begin(), i), advanced(M.ref.begin(), j)); + } + break; + } + case 18: { + A.lst.swap(B.lst); + A.ref.swap(B.ref); + break; + } + case 19: { + if (&M == &A) { A.lst = B.lst; A.ref = B.ref; } + else { B.lst = A.lst; B.ref = A.ref; } + break; + } + case 20: { + if (&M == &A) { A.lst = std::move(B.lst); A.ref = std::move(B.ref); } + else { B.lst = std::move(A.lst); B.ref = std::move(A.ref); } + break; + } + case 21: { + int v = valueDist(rng); + if (rng() % 2 == 0) { M.lst.emplace_back(v); M.ref.emplace_back(v); } + else { M.lst.emplace_front(v); M.ref.emplace_front(v); } + break; + } + } + + if (iter % 25 == 0) { + A.check("A @ iter " + std::to_string(iter)); + B.check("B @ iter " + std::to_string(iter)); + } + } + + A.check("A final"); + B.check("B final"); +} + +int main(int argc, char** argv) { + std::uint32_t seed = argc > 1 ? static_cast(std::stoul(argv[1])) : 1u; + int iterations = argc > 2 ? std::stoi(argv[2]) : 20000; + + std::cout << "== deterministic edge-case tests ==\n"; + std::cout << "-> test_basic_push_pop\n" << std::flush; test_basic_push_pop(); + std::cout << "-> test_copy_and_move\n" << std::flush; test_copy_and_move(); + std::cout << "-> test_self_assignment_and_self_swap\n" << std::flush; test_self_assignment_and_self_swap(); + std::cout << "-> test_erase_and_ranges\n" << std::flush; test_erase_and_ranges(); + std::cout << "-> test_insert_variants\n" << std::flush; test_insert_variants(); + std::cout << "-> test_remove_remove_if_unique\n" << std::flush; test_remove_remove_if_unique(); + std::cout << "-> test_sort_stability_and_reverse\n" << std::flush; test_sort_stability_and_reverse(); + std::cout << "-> test_splice_whole_and_element_and_range\n" << std::flush; test_splice_whole_and_element_and_range(); + std::cout << "-> test_resize\n" << std::flush; test_resize(); + std::cout << "-> test_const_iterator_conversion_and_reverse_iterators\n" << std::flush; test_const_iterator_conversion_and_reverse_iterators(); + std::cout << "-> deterministic tests done\n" << std::flush; + std::cout << " checks so far: " << g_checks + << ", failures: " << g_failures << "\n"; + + std::cout << "== differential fuzz test vs std::list ==\n"; + std::cout << " seed=" << seed << " iterations=" << iterations << "\n"; + fuzz_run(seed, iterations); + + for (std::uint32_t s = seed + 1; s < seed + 5; ++s) { + fuzz_run(s, std::max(2000, iterations / 4)); + } + + std::cout << "\n== summary ==\n"; + std::cout << "checks total: " << g_checks << "\n"; + std::cout << "failures: " << g_failures << "\n"; + + if (g_failures > 0) { + std::cout << "RESULT: FAIL\n"; + return 1; + } + std::cout << "RESULT: OK\n"; + return 0; +} \ No newline at end of file