subsurface/core/owning_table.h
Berthold Stoeger e39dea3d68 core: replace divesite_table_t by a vector of std::unique_ptr<>s
This is a long commit, because it introduces a new abstraction:
a general std::vector<> of std::unique_ptrs<>.

Moreover, it replaces a number of pointers by C++ references,
when the callee does not suppoert null objects.

This simplifies memory management and makes ownership more
explicit. It is a proof-of-concept and a test-bed for
the other core data structrures.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-08-13 19:28:30 +02:00

153 lines
5.5 KiB
C++

// SPDX-License-Identifier: GPL-2.0
/* Traditionally, most of our data structures are collected as tables
* (=arrays) of pointers. The advantage is that pointers (or references)
* to objects are stable, even if the array grows and reallocates.
* Implicitly, the table is the owner of the objects and the objets
* are deleted (freed) when removed from the table.
* This header defines an std::vector<> of unique_ptr<>s to make this
* explicit.
*
* The state I want to reach across the code base: whenever a part of the
* code owns a heap allocated object, it *always* possesses a unique_ptr<>
* to that object. All naked pointers are invariably considered as
* "non owning".
*
* There are two ways to end ownership:
* 1) The std::unique_ptr<> goes out of scope and the object is
* automatically deleted.
* 2) Ownership is passed to another std::unique_ptr<> using std::move().
*
* This means that when adding an object to an owning_table,
* ownership of a std::unique_ptr<> is given up with std::move().
* The table then returns a non-owning pointer to the object and
* optionally the insertion index.
*
* In converse, when removing an object, one provides a non-owning
* pointer, which is turned into an owning std::unique_ptr<> and
* the index where the object was removed.
* When ignoring the returned owning pointer, the object is
* automatically freed.
*
* The functions to add an entry to the table are called "put()",
* potentially with a suffix. The functions to remove an entry
* are called "pull()", likewise with an optional suffix.
*
* There are two versions of the table:
* 1) An unordered version, where the caller is responsible for
* adding at specified positions (either given by an index or at the end).
* Removal via a non-owning pointer is implemented by a linear search
* over the whole table.
* 2) An ordered version, where a comparison function that returns -1, 0, 1
* is used to add objects. In that case, the caller must make sure that
* no two ojects that compare equal are added to the table.
* Obviously, the compare function is supposed to be replaced by the
* "spaceship operator" in due course.
* Here, adding and removing via non-owning pointers is implemented
* using a binary search.
*
* Note that, since the table contains std::unique_ptr<>s, to loop over
* all entries, it is best to use something such as
* for (const auto &ptr: table) ...
* I plan to add iterator adapters, that autometically dereference
* the unique_ptr<>s and provide const-references for const-tables.
*
* Time will tell how useful this class is.
*/
#ifndef CORE_OWNING_TABLE_H
#define CORE_OWNING_TABLE_H
#include "errorhelper.h"
#include <algorithm>
#include <memory>
#include <string>
#include <vector>
template <typename T>
class owning_table : public std::vector<std::unique_ptr<T>> {
public:
struct put_result {
T *ptr;
size_t idx;
};
struct pull_result {
std::unique_ptr<T> ptr;
size_t idx;
};
size_t get_idx(const T *item) const {
auto it = std::find_if(this->begin(), this->end(),
[item] (auto &i1) {return i1.get() == item; });
return it != this->end() ? it - this->begin() : std::string::npos;
}
T *put_at(std::unique_ptr<T> item, size_t idx) {
T *res = item.get();
insert(this->begin() + idx, std::move(item));
return res;
}
// Returns index of added item
put_result put_back(std::unique_ptr<T> item) {
T *ptr = item.get();
push_back(std::move(item));
return { ptr, this->size() - 1 };
}
std::unique_ptr<T> pull_at(size_t idx) {
auto it = this->begin() + idx;
std::unique_ptr<T> res = std::move(*it);
this->erase(it);
return res;
}
pull_result pull(const T *item) {
size_t idx = get_idx(item);
if (idx == std::string::npos) {
report_info("Warning: removing unexisting item in %s", __func__);
return { std::unique_ptr<T>(), std::string::npos };
}
return { pull_at(idx), idx };
}
};
// Note: there must not be any elements that compare equal!
template <typename T, int (*CMP)(const T &, const T &)>
class sorted_owning_table : public owning_table<T> {
public:
using typename owning_table<T>::put_result;
using typename owning_table<T>::pull_result;
// Returns index of added item
put_result put(std::unique_ptr<T> item) {
auto it = std::lower_bound(this->begin(), this->end(), item,
[] (const auto &i1, const auto &i2)
{ return CMP(*i1, *i2) < 0; });
if (it != this->end() && CMP(**it, *item) == 0)
report_info("Warning: adding duplicate item in %s", __func__);
size_t idx = it - this->begin();
T *ptr = item.get();
this->insert(it, std::move(item));
return { ptr, idx };
}
// Optimized version of get_idx(), which uses binary search
// If not found, fall back to linear search and emit a warning.
// Note: this is probaly slower than a linesr search. But for now,
// it helps finding consistency problems.
size_t get_idx(const T *item) const {
auto it = std::lower_bound(this->begin(), this->end(), item,
[] (const auto &i1, const auto &i2)
{ return CMP(*i1, *i2) < 0; });
if (it == this->end() || CMP(**it, *item) != 0) {
size_t idx = owning_table<T>::get_idx(item);
if (idx != std::string::npos)
report_info("Warning: index found by linear but not by binary search in %s", __func__);
return idx;
}
return it - this->begin();
}
pull_result pull(const T *item) {
size_t idx = get_idx(item);
if (idx == std::string::npos) {
report_info("Warning: removing unexisting item in %s", __func__);
return { std::unique_ptr<T>(), std::string::npos };
}
return { this->pull_at(idx), idx };
}
};
#endif