foundationdb/flow/Deque.h

238 lines
6.2 KiB
C++

/*
* Deque.h
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef FLOW_DEQUE_H
#define FLOW_DEQUE_H
#pragma once
#include "flow/Platform.h"
#include <stdexcept>
template <class T>
class Deque {
// Double ended queue implemented using circular array (and on-demand reallocation, like std::vector)
// Interface similar to std::deque, but incomplete (also reallocation invalidates all iterators like std::vector)
// Capacity is limited to 2^32-1 items even in 64 bit
public:
typedef T value_type;
typedef T& reference;
typedef T const& const_reference;
typedef int32_t difference_type;
typedef uint32_t size_type;
Deque() : arr(0), begin(0), end(0), mask(-1) {}
// TODO: iterator construction, other constructors
Deque(Deque const& r) : arr(nullptr), begin(0), end(r.size()), mask(r.mask) {
if (r.capacity() > 0) {
arr = (T*)aligned_alloc(std::max(__alignof(T), sizeof(void*)), capacity() * sizeof(T));
if (arr == nullptr) {
platform::outOfMemory();
}
}
ASSERT(capacity() >= end || end == 0);
if (r.end < r.capacity()) {
std::copy(r.arr + r.begin, r.arr + r.begin + r.size(), arr);
} else {
// r.begin is always < capacity(), and r.end is always >= r.begin. Mask is used for wrapping r.end.
// but if r.end >= r.capacity(), the deque wraps around so the
// copy must be performed in two parts
auto partTwo = std::copy(r.arr + r.begin, r.arr + r.capacity(), arr);
std::copy(r.arr, r.arr + (r.end & r.mask), partTwo);
}
}
void operator=(Deque const& r) {
cleanup();
arr = nullptr;
begin = 0;
end = r.size();
mask = r.mask;
if (r.capacity() > 0) {
arr = (T*)aligned_alloc(std::max(__alignof(T), sizeof(void*)), capacity() * sizeof(T));
if (arr == nullptr) {
platform::outOfMemory();
}
}
ASSERT(capacity() >= end || end == 0);
if (r.end < r.capacity()) {
std::copy(r.arr + r.begin, r.arr + r.begin + r.size(), arr);
} else {
// r.begin is always < capacity(), and r.end is always >= r.begin. Mask is used for wrapping r.end.
// but if r.end >= r.capacity(), the deque wraps around so the
// copy must be performed in two parts
auto partTwo = std::copy(r.arr + r.begin, r.arr + r.capacity(), arr);
std::copy(r.arr, r.arr + (r.end & r.mask), partTwo);
}
}
Deque(Deque&& r) noexcept : arr(r.arr), begin(r.begin), end(r.end), mask(r.mask) {
r.arr = nullptr;
r.begin = r.end = 0;
r.mask = -1;
}
void operator=(Deque&& r) noexcept {
cleanup();
begin = r.begin;
end = r.end;
mask = r.mask;
arr = r.arr;
r.arr = nullptr;
r.begin = r.end = 0;
r.mask = -1;
}
bool operator==(const Deque& r) const {
if (size() != r.size())
return false;
for (uint32_t i = 0; i < size(); i++)
if ((*this)[i] != r[i])
return false;
return true;
}
bool operator!=(const Deque& r) const { return !(*this == r); }
~Deque() { cleanup(); }
void push_back(const T& val) {
if (full())
grow();
new (&arr[end & mask]) T(val);
end++;
}
template <class... U>
reference emplace_back(U&&... val) {
if (full())
grow();
new (&arr[end & mask]) T(std::forward<U>(val)...);
reference result = arr[end & mask];
end++;
return result;
}
void pop_back() {
ASSERT(!empty());
end--;
arr[end & mask].~T();
}
void pop_front() {
ASSERT(!empty());
arr[begin].~T();
if (begin == mask) {
begin -= mask;
end -= mask + 1;
} else
begin++;
}
void clear() {
for (uint32_t i = begin; i != end; i++)
arr[i & mask].~T();
begin = end = 0;
}
size_type size() const { return end - begin; }
bool empty() const { return end == begin; }
size_type capacity() const { return mask + 1; }
size_type max_size() const {
return 1 << 30;
} // All the logic should work at size 2^32, but size() can't return it, and callers might break, and there might be
// bugs...
T& front() { return arr[begin]; }
T const& front() const { return arr[begin]; }
T& back() { return arr[(end - 1) & mask]; }
T const& back() const { return arr[(end - 1) & mask]; }
T& operator[](int i) { return arr[(begin + i) & mask]; }
T const& operator[](int i) const { return arr[(begin + i) & mask]; }
T& at(int i) {
if (i < 0 || i >= end - begin)
throw std::out_of_range("requires 0 <= i < size");
return (*this)[i];
}
T const& at(int i) const {
if (i < 0 || i >= end - begin)
throw std::out_of_range("requires 0 <= i < size");
return (*this)[i];
}
private:
T* arr;
uint32_t begin, end, mask;
bool full() const { return end == begin + mask + 1; }
void grow() {
// This doubles capacity (or makes it at least 8), and arbitrarily moves begin to be 0
size_t mp1 = arr ? size_t(mask) + 1 : 4;
size_t newSize = mp1 * 2;
if (newSize > max_size())
throw std::bad_alloc();
// printf("Growing to %lld (%u-%u mask %u)\n", (long long)newSize, begin, end, mask);
T* newArr = (T*)aligned_alloc(std::max(__alignof(T), sizeof(void*)),
newSize * sizeof(T)); // SOMEDAY: FastAllocator
if (newArr == nullptr) {
platform::outOfMemory();
}
for (int i = begin; i != end; i++) {
try {
new (&newArr[i - begin]) T(std::move_if_noexcept(arr[i & mask]));
} catch (...) {
cleanup(newArr, i - begin);
throw;
}
}
for (int i = begin; i != end; i++) {
static_assert(std::is_nothrow_destructible_v<T>);
arr[i & mask].~T();
}
aligned_free(arr);
arr = newArr;
end -= begin;
begin = 0;
mask = uint32_t(newSize - 1);
}
static void cleanup(T* data, size_t size) noexcept {
for (int i = 0; i < size; ++i) {
data[i].~T();
}
aligned_free(data);
}
void cleanup() noexcept {
for (int i = begin; i != end; i++)
arr[i & mask].~T();
if (arr)
aligned_free(arr);
}
};
#endif