Files
scylladb/utils/double-decker.hh
Kefu Chai f5b05cf981 treewide: use defaulted operator!=() and operator==()
in C++20, compiler generate operator!=() if the corresponding
operator==() is already defined, the language now understands
that the comparison is symmetric in the new standard.

fortunately, our operator!=() is always equivalent to
`! operator==()`, this matches the behavior of the default
generated operator!=(). so, in this change, all `operator!=`
are removed.

in addition to the defaulted operator!=, C++20 also brings to us
the defaulted operator==() -- it is able to generated the
operator==() if the member-wise lexicographical comparison.
under some circumstances, this is exactly what we need. so,
in this change, if the operator==() is also implemented as
a lexicographical comparison of all memeber variables of the
class/struct in question, it is implemented using the default
generated one by removing its body and mark the function as
`default`. moreover, if the class happen to have other comparison
operators which are implemented using lexicographical comparison,
the default generated `operator<=>` is used in place of
the defaulted `operator==`.

sometimes, we fail to mark the operator== with the `const`
specifier, in this change, to fulfil the need of C++ standard,
and to be more correct, the `const` specifier is added.

also, to generate the defaulted operator==, the operand should
be `const class_name&`, but it is not always the case, in the
class of `version`, we use `version` as the parameter type, to
fulfill the need of the C++ standard, the parameter type is
changed to `const version&` instead. this does not change
the semantic of the comparison operator. and is a more idiomatic
way to pass non-trivial struct as function parameters.

please note, because in C++20, both operator= and operator<=> are
symmetric, some of the operators in `multiprecision` are removed.
they are the symmetric form of the another variant. if they were
not removed, compiler would, for instance, find ambiguous
overloaded operator '=='.

this change is a cleanup to modernize the code base with C++20
features.

Signed-off-by: Kefu Chai <kefu.chai@scylladb.com>

Closes #13687
2023-04-27 10:24:46 +03:00

399 lines
13 KiB
C++

/*
* Copyright (C) 2020-present ScyllaDB
*/
/*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <type_traits>
#include <seastar/util/concepts.hh>
#include "utils/bptree.hh"
#include "utils/intrusive-array.hh"
#include "utils/collection-concepts.hh"
#include <fmt/core.h>
/*
* The double-decker is the ordered keeper of key:value pairs having
* the pairs sorted by both key and value (key first).
*
* The keys collisions are expected to be rare enough to afford holding
* the values in a sorted array with the help of linear algorithms.
*/
template <typename Key, typename T, typename Less, typename Compare, int NodeSize,
bplus::key_search Search = bplus::key_search::binary, bplus::with_debug Debug = bplus::with_debug::no>
requires Comparable<T, T, Compare> && std::is_nothrow_move_constructible_v<T>
class double_decker {
public:
using inner_array = intrusive_array<T>;
using outer_tree = bplus::tree<Key, inner_array, Less, NodeSize, Search, Debug>;
using outer_iterator = typename outer_tree::iterator;
using outer_const_iterator = typename outer_tree::const_iterator;
private:
outer_tree _tree;
public:
template <bool Const>
class iterator_base {
friend class double_decker;
using outer_iterator = std::conditional_t<Const, typename double_decker::outer_const_iterator, typename double_decker::outer_iterator>;
protected:
outer_iterator _bucket;
int _idx;
public:
iterator_base() = default;
iterator_base(outer_iterator bkt, int idx) noexcept : _bucket(bkt), _idx(idx) {}
using iterator_category = std::bidirectional_iterator_tag;
using difference_type = ssize_t;
using value_type = std::conditional_t<Const, const T, T>;
using pointer = value_type*;
using reference = value_type&;
reference operator*() const noexcept { return (*_bucket)[_idx]; }
pointer operator->() const noexcept { return &((*_bucket)[_idx]); }
iterator_base& operator++() noexcept {
if ((*_bucket)[_idx++].is_tail()) {
_bucket++;
_idx = 0;
}
return *this;
}
iterator_base operator++(int) noexcept {
iterator_base cur = *this;
operator++();
return cur;
}
iterator_base& operator--() noexcept {
if (_idx-- == 0) {
_bucket--;
_idx = _bucket->index_of(_bucket->end()) - 1;
}
return *this;
}
iterator_base operator--(int) noexcept {
iterator_base cur = *this;
operator--();
return cur;
}
bool operator==(const iterator_base& o) const noexcept = default;
};
using const_iterator = iterator_base<true>;
class iterator final : public iterator_base<false> {
friend class double_decker;
using super = iterator_base<false>;
iterator(const const_iterator&& other) noexcept : super(std::move(other._bucket), other._idx) {}
public:
iterator() noexcept : super() {}
iterator(outer_iterator bkt, int idx) noexcept : super(bkt, idx) {}
iterator(T* ptr) noexcept {
inner_array& arr = inner_array::from_element(ptr, super::_idx);
super::_bucket = outer_iterator(&arr);
}
template <typename Func>
requires Disposer<Func, T>
iterator erase_and_dispose(Less less, Func&& disp) noexcept {
disp(&**this); // * to deref this, * to call operator*, & to get addr from ref
if (super::_bucket->is_single_element()) {
outer_iterator bkt = super::_bucket.erase(less);
return iterator(bkt, 0);
}
bool tail = (*super::_bucket)[super::_idx].is_tail();
super::_bucket->erase(super::_idx);
if (tail) {
super::_bucket++;
super::_idx = 0;
}
return *this;
}
iterator erase(Less less) noexcept { return erase_and_dispose(less, bplus::default_dispose<T>); }
};
/*
* Structure that shed some more light on how the lower_bound
* actually found the bounding elements.
*/
struct bound_hint {
/*
* Set to true if the element fully matched to the key
* according to Compare
*/
bool match;
/*
* Set to true if the bucket for the given key exists
*/
bool key_match;
/*
* Set to true if the given key is more than anything
* on the bucket and iterator was switched to the next
* one (or when the key_match is false)
*/
bool key_tail;
/*
* This helper says whether the emplace will invalidate (some)
* iterators or not. Emplacing with !key_match will go and create
* new node in B+ which doesn't invalidate iterators. In another
* case some existing B+ data node will be reconstructed, so the
* iterators on those nodes will become invalid.
*/
bool emplace_keeps_iterators() const noexcept { return !key_match; }
};
iterator begin() noexcept { return iterator(_tree.begin(), 0); }
const_iterator begin() const noexcept { return const_iterator(_tree.begin(), 0); }
const_iterator cbegin() const noexcept { return const_iterator(_tree.begin(), 0); }
iterator end() noexcept { return iterator(_tree.end(), 0); }
const_iterator end() const noexcept { return const_iterator(_tree.end(), 0); }
const_iterator cend() const noexcept { return const_iterator(_tree.end(), 0); }
explicit double_decker(Less less) noexcept : _tree(less) { }
double_decker(const double_decker& other) = delete;
double_decker(double_decker&& other) noexcept : _tree(std::move(other._tree)) {}
iterator insert(Key k, T value, Compare cmp) {
std::pair<outer_iterator, bool> oip = _tree.emplace(std::move(k), std::move(value));
outer_iterator& bkt = oip.first;
int idx = 0;
if (!oip.second) {
/*
* Unlikely, but in this case we reconstruct the array. The value
* must not have been moved by emplace() above.
*/
idx = bkt->index_of(bkt->lower_bound(value, cmp));
size_t new_size = (bkt->size() + 1) * sizeof(T);
bkt.reconstruct(new_size, *bkt,
typename inner_array::grow_tag{idx}, std::move(value));
}
return iterator(bkt, idx);
}
template <typename... Args>
iterator emplace_before(iterator i, Key k, const bound_hint& hint, Args&&... args) {
assert(!hint.match);
outer_iterator& bucket = i._bucket;
if (!hint.key_match) {
/*
* The most expected case -- no key conflict, respectively the
* bucket is not found, and i points to the next one. Just go
* ahead and emplace the new bucket before the i and push the
* 0th element into it.
*/
outer_iterator nb = bucket.emplace_before(std::move(k), _tree.less(), std::forward<Args>(args)...);
return iterator(nb, 0);
}
/*
* Key conflict, need to expand some inner vector, but still there
* are two cases -- whether the bounding element is on k's bucket
* or the bound search overflew and switched to the next one.
*/
int idx = i._idx;
if (hint.key_tail) {
/*
* The latter case -- i points to the next one. Need to shift
* back and append the new element to its tail.
*/
bucket--;
idx = bucket->index_of(bucket->end());
}
size_t new_size = (bucket->size() + 1) * sizeof(T);
bucket.reconstruct(new_size, *bucket,
typename inner_array::grow_tag{idx}, std::forward<Args>(args)...);
return iterator(bucket, idx);
}
template <typename K = Key>
requires Comparable<K, T, Compare>
const_iterator find(const K& key, Compare cmp) const {
outer_const_iterator bkt = _tree.find(key);
int idx = 0;
if (bkt != _tree.end()) {
bool match = false;
idx = bkt->index_of(bkt->lower_bound(key, cmp, match));
if (!match) {
bkt = _tree.end();
idx = 0;
}
}
return const_iterator(bkt, idx);
}
template <typename K = Key>
requires Comparable<K, T, Compare>
iterator find(const K& k, Compare cmp) {
return iterator(const_cast<const double_decker*>(this)->find(k, std::move(cmp)));
}
template <typename K = Key>
requires Comparable<K, T, Compare>
const_iterator lower_bound(const K& key, Compare cmp, bound_hint& hint) const {
outer_const_iterator bkt = _tree.lower_bound(key, hint.key_match);
hint.key_tail = false;
hint.match = false;
if (bkt == _tree.end() || !hint.key_match) {
return const_iterator(bkt, 0);
}
int i = bkt->index_of(bkt->lower_bound(key, cmp, hint.match));
if (i != 0 && (*bkt)[i - 1].is_tail()) {
/*
* The lower_bound is after the last element -- shift
* to the net bucket's 0'th one.
*/
bkt++;
i = 0;
hint.key_tail = true;
}
return const_iterator(bkt, i);
}
template <typename K = Key>
requires Comparable<K, T, Compare>
iterator lower_bound(const K& key, Compare cmp, bound_hint& hint) {
return iterator(const_cast<const double_decker*>(this)->lower_bound(key, std::move(cmp), hint));
}
template <typename K = Key>
requires Comparable<K, T, Compare>
const_iterator lower_bound(const K& key, Compare cmp) const {
bound_hint hint;
return lower_bound(key, cmp, hint);
}
template <typename K = Key>
requires Comparable<K, T, Compare>
iterator lower_bound(const K& key, Compare cmp) {
return iterator(const_cast<const double_decker*>(this)->lower_bound(key, std::move(cmp)));
}
template <typename K = Key>
requires Comparable<K, T, Compare>
const_iterator upper_bound(const K& key, Compare cmp) const {
bool key_match;
outer_const_iterator bkt = _tree.lower_bound(key, key_match);
if (bkt == _tree.end() || !key_match) {
return const_iterator(bkt, 0);
}
int i = bkt->index_of(bkt->upper_bound(key, cmp));
if (i != 0 && (*bkt)[i - 1].is_tail()) {
// Beyond the end() boundary
bkt++;
i = 0;
}
return const_iterator(bkt, i);
}
template <typename K = Key>
requires Comparable<K, T, Compare>
iterator upper_bound(const K& key, Compare cmp) {
return iterator(const_cast<const double_decker*>(this)->upper_bound(key, std::move(cmp)));
}
template <typename Func>
requires Disposer<Func, T>
void clear_and_dispose(Func&& disp) noexcept {
_tree.clear_and_dispose([&disp] (inner_array* arr) noexcept {
arr->for_each(disp);
});
}
void clear() noexcept { clear_and_dispose(bplus::default_dispose<T>); }
template <typename Func>
requires Disposer<Func, T>
iterator erase_and_dispose(iterator begin, iterator end, Func&& disp) noexcept {
bool same_bucket = begin._bucket == end._bucket;
// Drop the tail of the starting bucket if it's not fully erased
while (begin._idx != 0) {
if (same_bucket) {
if (begin == end) {
return begin;
}
end._idx--;
}
begin = begin.erase_and_dispose(_tree.less(), disp);
}
// Drop all the buckets in between
outer_iterator nb = _tree.erase_and_dispose(begin._bucket, end._bucket, [&disp] (inner_array* arr) noexcept {
arr->for_each(disp);
});
assert(nb == end._bucket);
/*
* Drop the head of the ending bucket. Every erased element is the 0th
* one, when erased it will shift the rest left and reconstruct the array,
* thus we cannot rely on the end to keep neither _bucket not _idx.
*
* Said that -- just erase the required number of elements. A corner case
* when end points to the tree end is handled, _idx is 0 in this case.
*/
iterator next(nb, 0);
while (end._idx-- != 0) {
next = next.erase_and_dispose(_tree.less(), disp);
}
return next;
}
iterator erase(iterator begin, iterator end) noexcept {
return erase_and_dispose(begin, end, bplus::default_dispose<T>);
}
bool empty() const noexcept { return _tree.empty(); }
static size_t estimated_object_memory_size_in_allocator(allocation_strategy& allocator, const T* obj) noexcept {
/*
* The T-s are merged together in array, so getting any run-time
* value of a pointer would be wrong. So here's some guessing of
* how much memory would this thing occupy in memory
*/
return sizeof(typename outer_tree::data);
}
};