I'm not sure if this is a good question or not — please close it if not.
I set out to write (using boost::coordinate_vector as a starting point) a sparse_vector template class that efficiently implements a vector-like interface, but is sparse. It implements all the usual vector operations and a fast sparse iterator that iterates over the set elements. It also has a fast version of rotate. I need this class because I have a write-once-read-many use case, and I use many of these sparse_vectors.
I have no experience writing template classes, so I know that there are things that I could be doing better. How can I improve this class?
// sparse_vector is a sparse version of std::vector. It stores index-value pairs, and a size, which
// represents the size of the sparse_vector.
#include <algorithm>
#include <ostream>
#include <iostream>
#include <vector>
#include <boost/iterator.hpp>
#include <boost/iterator/iterator_adaptor.hpp>
#include <boost/iterator/iterator_facade.hpp>
#include <boost/iterator/reverse_iterator.hpp>
#include <boost/optional.hpp>
#include <glog/logging.h>
template<typename T>
class sparse_vector {
public:
typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef const T& const_reference;
typedef size_t size_type;
private:
struct ElementType {
ElementType(size_type i, T const& t): index(i), value(t) {}
ElementType(size_type i, T&& t): index(i), value(move(t)) {}
ElementType(size_type i): index(i) {} // allows lower_bound to work
ElementType(ElementType const&) = default;
size_type index;
value_type value;
};
typedef std::vector<ElementType> array_type;
public:
typedef typename array_type::difference_type difference_type;
private:
typedef const T* const_pointer;
typedef sparse_vector<T> self_type;
typedef typename array_type::const_iterator const_subiterator_type;
typedef typename array_type::iterator subiterator_type;
struct IndexCompare {
bool operator()(ElementType const& a, ElementType const& b) { return a.index < b.index; }
};
public:
// Construction and destruction
inline sparse_vector(): size_(0), sorted_filled_(0), data_() {
storage_invariants(); }
explicit inline sparse_vector(size_type size): size_(size), sorted_filled_(0), data_() {
storage_invariants(); }
inline sparse_vector(const sparse_vector<T> &v):
size_(v.size_), sorted_filled_(v.sorted_filled_), data_(v.data_) {
storage_invariants(); }
inline sparse_vector(sparse_vector<T>&& v):
size_(v.size_), sorted_filled_(v.sorted_filled_), data_(move(v.data_)) {
storage_invariants(); }
sparse_vector(const optional<T> &o): size_(1), sorted_filled_(0) {
if (o) {
append_element(0, *o);
sorted_filled_ = 1;
}
storage_invariants();
}
sparse_vector(const std::vector<T> &v):
size_(v.size()), sorted_filled_(0) {
data_.reserve(v.size());
for (auto it = v.begin(); it != v.end(); ++it)
append_element(it - v.begin(), *it);
sorted_filled_ = v.size();
storage_invariants();
}
sparse_vector(const sparse_vector<optional<T>> &sv):
size_(sv.size()), sorted_filled_(0) {
for (auto it = sv.sparse_begin(); it != sv.sparse_end(); ++it)
if (*it)
append_element(it.index(), **it);
sorted_filled_ = data_.size();
storage_invariants();
}
sparse_vector(size_type size, vector<pair<size_type, T>> init_list):
size_(size), sorted_filled_(0) {
for (auto it = init_list.begin(); it != init_list.end(); ++it)
append_element(it->first, it->second);
// require that the list be sorted?
// sorted_filled_ = data_.size();
storage_invariants();
}
/*template<class AE>
inline sparse_vector(const sparse_vector<AE> &ae):
size_(ae().size()), sorted_filled_(ae.nnz()), data_() {
storage_invariants();
for (auto it = ae.sparse_begin(); it != ae.sparse_end(); ++it) {
(*this)[it.index()] = static_cast<T>(*it);
}
}*/
// Conversion operators
// Copy sparse_vector to a vector filling in uninitialized elements with T()
operator std::vector<T>() {
std::vector<T> retval(size_);
for (auto it = data_.begin(); it != data_.end(); ++it)
retval[it.index()] = *it;
return retval;
}
// Copy sparse_vector to a vector filling in uninitialized elements with a default value.
void CopyToVector(std::vector<T>* v, T const& default_value = T()) {
size_type last_index = 0;
for (auto it = sparse_begin(); it != sparse_end(); ++it) {
for (size_type i = last_index; i < it.index(); ++i) {
(*v)[i] = default_value;
}
(*v)[it.index()] = *it;
last_index = it.index() + 1;
}
}
// Accessors
inline size_type size() const {
return size_;
}
inline size_type nnz_capacity() const {
return data_.capacity();
}
inline size_type nnz() const {
return data_.size();
}
// Resizing
inline void resize(size_type size, bool preserve = true) {
if (preserve) {
sort(); // remove duplicate elements.
subiterator_type it(lower_bound(data_.begin(), data_.end(), size, IndexCompare()));
data_.erase(it, data_.end());
} else {
data_.clear();
}
size_ = size;
sorted_filled_ = nnz();
storage_invariants();
}
// Reserving
inline void reserve(size_type non_zeros, bool preserve = true) {
if (preserve)
sort(); // remove duplicate elements.
else
data_.clear();
data_.reserve(non_zeros);
sorted_filled_ = nnz();
storage_invariants();
}
// Element support
// find_element returns a pointer to element i or NULL -- it never creates an element
inline pointer find_element(size_type i) {
return const_cast<pointer> (const_cast<const self_type&>(*this).find_element(i)); }
inline const_pointer find_element(size_type i) const {
sort();
const_subiterator_type it(lower_bound(data_.begin(), data_.end(), i, IndexCompare()));
if (it == data_.end() || it->index != i)
return NULL;
return &it->value;
}
// find_element_optional returns a boost::optional<T>
inline boost::optional<value_type> find_element_optional(size_type i) const {
CHECK_LT(i, size_);
sort();
const_subiterator_type it(lower_bound(data_.begin(), data_.end(), i, IndexCompare()));
if (it == data_.end() || it->index != i)
return boost::optional<value_type>();
return boost::optional<value_type>(it->value);
}
// Element access
// operator[] returns a reference to element i -- the const version returns a reference to
// a zero_ element if it doesn't exist; the non-const version creates it in that case.
inline const_reference operator[] (size_type i) const {
CHECK_LT(i, size_);
sort();
const_subiterator_type it(lower_bound(data_.begin(), data_.end(), i, IndexCompare()));
if (it == data_.end() || it->index != i)
return zero_;
return it->value;
}
inline reference operator[] (size_type i) {
CHECK_LT(i, size_);
sort();
subiterator_type it(lower_bound(data_.begin(), data_.end(), i, IndexCompare()));
if (it == data_.end() || it->index != i)
return insert_element(i, value_type/*zero*/());
else
return it->value;
}
// Element assignment
inline void append_element(size_type i, const_reference t) {
data_.emplace_back(i, t);
storage_invariants();
}
inline void append_element(size_type i, T&& t) {
data_.emplace_back(i, move(t));
storage_invariants();
}
inline void sorted_append_element(size_type i, const_reference t) {
// must maintain sort order
CHECK(sorted());
if (data_.size() > 0)
CHECK_LT(data_.back().index, i);
data_.emplace_back(i, t);
sorted_filled_ = data_.size();
storage_invariants();
}
/* This function is probably not useful.
* inline void sparse_pop_back() {
// ISSUE invariants could be simpilfied if sorted required as precondition
CHECK_GT(data_.size(), 0);
data_.pop_back();
sorted_filled_ = (std::min)(sorted_filled_, data_.size());
storage_invariants();
}*/
inline reference insert_element(size_type i, const_reference t) {
DCHECK(find_element(i) == NULL); // duplicate element
append_element(i, t);
return data_.back().value;
}
// Element clearing
// TODO(neil): consider replacing size_type functions with iterator functions
// replaces elements in the range [i, i] with "zero" (by erasing them from the map)
inline void clear_element(size_type i) {
sort();
subiterator_type it(lower_bound(data_.begin(), data_.end(), i, IndexCompare()));
if (it == data_.end() || it->index != i)
return;
data_.erase(it);
--sorted_filled_;
storage_invariants();
}
// replaces elements in the range [first, last) with "zero" (by erasing them from the map)
inline void clear_elements(size_type first, size_type last) {
CHECK_LE(first, last);
sort();
subiterator_type it_first(lower_bound(data_.begin(), data_.end(), first, IndexCompare()));
subiterator_type it_last(lower_bound(data_.begin(), data_.end(), last, IndexCompare()));
sorted_filled_ -= it_last - it_first;
data_.erase(it_first, it_last);
storage_invariants();
}
// Zeroing
inline void clear() { data_.clear(); sorted_filled_ = 0; storage_invariants(); }
// Comparison
inline bool operator==(const sparse_vector &v) const {
if (this == &v)
return true;
if (size_ != v.size_)
return false;
auto it_this = sparse_begin();
for (auto it = v.sparse_begin(); it != v.sparse_end(); ++it) {
if (it_this == sparse_end()
|| it_this.index() != it.index()
|| *it_this != *it)
return false;
++it_this;
}
return true;
}
// Assignment
inline sparse_vector& operator=(const sparse_vector &v) {
if (this != &v) {
size_ = v.size_;
sorted_filled_ = v.sorted_filled_;
data_ = v.data_;
}
storage_invariants();
return *this;
}
inline sparse_vector &assign_temporary(sparse_vector &v) {
swap(v);
return *this;
}
template<class AE>
inline sparse_vector& operator=(const sparse_vector<AE> &ae) {
self_type temporary(ae);
return assign_temporary(temporary);
}
// Swapping
inline void swap(sparse_vector &v) {
if (this != &v) {
std::swap(size_, v.size_);
std::swap(sorted_filled_, v.sorted_filled_);
data_.swap(v.data_);
}
storage_invariants();
}
inline friend void swap(sparse_vector &v1, sparse_vector &v2) { v1.swap(v2); }
// Sorting and summation of duplicates
inline bool sorted() const { return sorted_filled_ == data_.size(); }
inline void sort() const {
if (!sorted() && data_.size() > 0) {
// sort new elements and merge
std::sort(data_.begin() + sorted_filled_, data_.end(), IndexCompare());
std::inplace_merge(data_.begin(), data_.begin() + sorted_filled_, data_.end(),
IndexCompare());
// check for duplicates
size_type filled = 0;
for(size_type i = 1; i < data_.size(); ++i) {
if (data_[filled].index != data_[i].index) {
++filled;
if (filled != i) {
data_[filled] = data_[i];
}
} else {
CHECK(false); // duplicates
// value_data_[filled] += value_data_[i];
}
}
++filled;
sorted_filled_ = filled;
data_.erase(data_.begin() + filled, data_.end());
storage_invariants();
}
}
// Back element insertion and erasure
inline void push_back(const_reference t) {
CHECK(sorted());
data_.emplace_back(size(), t);
if (sorted_filled_ == data_.size())
++sorted_filled_;
++size_;
storage_invariants();
}
inline void pop_back() {
CHECK_GT(size_, 0);
clear_element(size_ - 1);
if (sorted_filled_ == size_)
--sorted_filled_;
--size_;
storage_invariants();
}
// Iterator types
private:
template<typename base_type, typename iterator_value_type>
class sparse_iterator_private
: public boost::iterator_adaptor<
sparse_iterator_private<base_type, iterator_value_type>, // Derived
base_type, // Base
iterator_value_type, // Value
boost::random_access_traversal_tag // CategoryOrTraversal
> {
private:
struct enabler {}; // a private type avoids misuse
public:
sparse_iterator_private()
: sparse_iterator_private<base_type, iterator_value_type>::iterator_adaptor_() {}
explicit sparse_iterator_private(base_type&& p)
: sparse_iterator_private<base_type, iterator_value_type>::iterator_adaptor_(p) {}
size_type index() const { return this->base()->index; }
iterator_value_type& value() const { return this->base()->value; }
private:
friend class boost::iterator_core_access;
iterator_value_type& dereference() const { return this->base()->value; }
};
template<typename container_type, typename iterator_value_type>
class iterator_private
: public boost::iterator_facade<
iterator_private<container_type, iterator_value_type>, // Derived
iterator_value_type, // Value
boost::random_access_traversal_tag, // CategoryOrTraversal
iterator_value_type&, // Reference
difference_type // Difference
> {
private:
struct enabler {}; // a private type avoids misuse
public:
iterator_private(): container_(NULL), index_(0) {}
iterator_private(container_type* container, size_type index)
: container_(container), index_(index) {}
iterator_private(iterator_private const& other)
: container_(other.container_), index_(other.index_) {}
iterator_private& operator=(iterator_private const& other) {
container_ = other.container_;
index_ = other.index_;
}
container_type& container() const { return *container_; }
size_type index() const { return index_; }
iterator_value_type& value() const { return dereference(); }
/* TODO(neil): how do you make this work?
template<typename U>
sparse_iterator_private<U> subsequent_sparse_iterator() {
return sparse_iterator_private<T>(lower_bound(container_.data_.begin(),
container_.data_.end(),
index_, IndexCompare()));
}
*/
private:
friend class boost::iterator_core_access;
iterator_value_type& dereference() const { return (*container_)[index_]; }
bool equal(iterator_private<container_type, iterator_value_type> const& other) const {
return index_ == other.index_ && container_ == other.container_; }
void increment() { ++index_; }
void decrement() { --index_; }
void advance(int n) { index_ += n; }
difference_type distance_to(iterator_private<container_type,
iterator_value_type> const& other) {
return index_ - other.index_; }
container_type* container_;
size_type index_;
};
public:
typedef sparse_iterator_private<typename array_type::iterator, value_type> sparse_iterator;
typedef sparse_iterator_private<
typename array_type::const_iterator, const value_type> const_sparse_iterator;
typedef iterator_private<sparse_vector, value_type> iterator;
typedef iterator_private<const sparse_vector, const value_type> const_iterator;
typedef boost::reverse_iterator<sparse_iterator> reverse_sparse_iterator;
typedef boost::reverse_iterator<const_sparse_iterator> const_reverse_sparse_iterator;
typedef boost::reverse_iterator<iterator> reverse_iterator;
typedef boost::reverse_iterator<const_iterator> const_reverse_iterator;
// Element lookup
// inline This function seems to be big. So we do not let the compiler inline it.
sparse_iterator sparse_find(size_type i) {
sort();
return sparse_iterator(lower_bound(data_.begin(), data_.end(), i, IndexCompare()));
}
// inline This function seems to be big. So we do not let the compiler inline it.
const_sparse_iterator sparse_find(size_type i) const {
sort();
return const_sparse_iterator(lower_bound(data_.begin(), data_.end(), i, IndexCompare()));
}
// the nth non-zero element
const_sparse_iterator nth_nonzero(size_type i) const {
CHECK_LE(data_.size(), i);
sort();
return const_sparse_iterator(data_.begin() + i);
}
// Forward iterators
inline sparse_iterator sparse_begin() { return sparse_find(0); }
inline sparse_iterator sparse_end() { return sparse_find(size_); }
inline const_sparse_iterator sparse_begin() const { return sparse_find(0); }
inline const_sparse_iterator sparse_end() const { return sparse_find(size_); }
inline iterator begin() { return iterator(this, 0); }
inline iterator end() { return iterator(this, size()); }
inline const_iterator begin() const { return const_iterator(this, 0); }
inline const_iterator end() const { return const_iterator(this, size()); }
// Reverse iterators
inline reverse_sparse_iterator sparse_rbegin() {
return reverse_sparse_iterator(sparse_end()); }
inline reverse_sparse_iterator sparse_rend() {
return reverse_sparse_iterator(sparse_begin()); }
inline const_reverse_sparse_iterator sparse_rbegin() const {
return const_reverse_sparse_iterator(sparse_end()); }
inline const_reverse_sparse_iterator sparse_rend() const {
return const_reverse_sparse_iterator(sparse_begin()); }
inline reverse_iterator rbegin() {
return reverse_iterator(end()); }
inline reverse_iterator rend() {
return reverse_iterator(begin()); }
inline const_reverse_iterator rbegin() const {
return const_reverse_iterator(end()); }
inline const_reverse_iterator rend() const {
return const_reverse_iterator(begin()); }
// Mutating algorithms
// like vector::erase (erases the elements from the container and shrinks the container)
inline void erase(iterator const& first, iterator const& last) {
clear_elements(first.index(), last.index());
CHECK(sorted());
subiterator_type it_first(lower_bound(data_.begin(), data_.end(),
first.index(), IndexCompare()));
size_t n = last.index() - first.index();
for (auto it = it_first; it != data_.end(); ++it)
it->index -= n;
size_ -= n;
}
// like vector::insert (insert elements into the container and causes the container to grow)
inline void insert(iterator const& index, size_type n) {
sort();
subiterator_type it_first(lower_bound(data_.begin(), data_.end(),
index.index(), IndexCompare()));
for (auto it = it_first; it != data_.end(); ++it)
it->index += n;
size_ += n;
}
// Removes all "zero" elements
void remove_zeros() {
size_type filled = 0;
for(size_type i = 0; i < data_.size(); ++i) {
if (i == sorted_filled_) {
// Once we've processed sorted_filled_ items, we know that whatever we've filled so
// far is sorted.
CHECK_LE(filled, sorted_filled_);
// Since sorted_filled_ only shrinks here, and i only grows, we won't enter this
// condition twice.
sorted_filled_ = filled;
}
if (data_[i].value != value_type/*zero*/()) {
if (filled != i) {
data_[filled] = data_[i];
}
++filled;
}
}
data_.erase(data_.begin() + filled, data_.end());
storage_invariants();
}
void rotate(size_type first, size_type middle, size_type last) {
CHECK_LT(first, middle);
CHECK_LT(middle, last);
sort();
subiterator_type it_first(lower_bound(data_.begin(), data_.end(),
first, IndexCompare()));
subiterator_type it_middle(lower_bound(data_.begin(), data_.end(),
middle, IndexCompare()));
subiterator_type it_last(lower_bound(data_.begin(), data_.end(),
last, IndexCompare()));
for (auto it = it_first; it != it_middle; ++it)
it->index += last - middle;
for (auto it = it_middle; it != it_last; ++it)
it->index -= middle - first;
std::rotate(it_first, it_middle, it_last);
// array remains sorted
}
sparse_vector<value_type> concatenate(sparse_vector<value_type> const& other) const {
sparse_vector<value_type> retval(size() + other.size());
for (auto it = sparse_begin(); it != sparse_end(); ++it) {
retval[it.index()] = *it;
}
for (auto it = other.sparse_begin(); it != other.sparse_end(); ++it) {
retval[it.index() + size()] = *it;
}
return retval;
}
private:
void storage_invariants() const {
CHECK_LE(sorted_filled_, data_.size());
if (sorted())
CHECK_EQ(sorted_filled_, data_.size());
else
CHECK_LT(sorted_filled_, data_.size());
if (!data_.empty())
CHECK_LT(data_.back().index, size_);
for (size_type i = 1; i < sorted_filled_; ++i)
CHECK_GT(data_[i].index, data_[i - 1].index);
}
size_type size_;
mutable typename array_type::size_type sorted_filled_;
mutable array_type data_;
static const value_type zero_;
};
template<typename T>
const typename sparse_vector<T>::value_type sparse_vector<T>::zero_ = T();
template<typename T>
ostream& operator<<(ostream& os, const sparse_vector<T>& v) {
os << "[" << v.size() << ": ";
for (auto it = v.sparse_begin(); it != v.sparse_end(); ++it) {
os << "(" << it.index() << ": " << it.value() << ") ";
}
return os << "]";
}
For someone with little experience writing class templates, you've created something fairly sophisticated. Unfortunately I can't do a in-depth analysis at the moment: it would take too much time giving the amount of code presented and the generality of the question. I can only briefly respond to an overview.
For a start, sorting mutable data on operator[] const and begin/end methods is kind of scary. This class won't be thread-safe even for concurrent read access. Dealing with multiple threads is probably beyond the scope of this class, but this behavior is very peculiar so it should probably be well-documented if the class is intended for very general-purpose use.
Another alternative is to require the client sort the vector explicitly and use linear access methods to fetch data until this is done. This isn't quite as automagic as your solution, but it will make your class safer to use across threads for read-purposes.
It also seems apparent to me that you did not inline this code with the aid of a profiler. Don't do this without the profiler's help. I'm not just spouting generalities here; I work with raytracers and profile code with vtune and parallel studio on a daily basis as part of my job and inlining code like this has a detrimental effect on performance. The most obvious case is due to code bloat, but there's a second, less obvious case that most people seem to overlook.
Even for things like single line accessors, if you inline them all, you'll interfere with the optimizer's ability to make the best use of registers and other optimizations. Optimizing compilers work best on a function-by-function basis with small, well-written functions. They generally don't do as good a job at an inter-function level and if you start inlining everything, the result is akin to one big flat function accessing all kinds of data which even ICC (Intel's optimizing compiler) doesn't do such a great job at dealing with. For a start, the compiler can't read your mind, so inlining everything makes less-executed branches optimized equally as well as more commonly executed ones (basically compromising the compiler's ability to optimize the common branches).
With the profiler, you can see these more commonly executed branches and inline them selectively, but never start by inlining: profilers do a good job at telling you what to consider inlining, but never what shouldn't be inlined. In fact, you'll wreak havoc on your ability to profile your code by inlining almost everything when you start.
As for public interface, I can see that the whole point of this is to implement contiguity and reduce memory and access times (using std::vector for the backend), but given its nature, I think you would do better to provide an interface similar to map<int, T>. The push_back and pop_back behavior, for instance, is rather confusing. Consider an alternative with insert and erase methods only and a non-const operator[] which can create new indices (keys), or do this at least as a first step. You could provide an insert method which only takes const_reference which chooses an available index to be assigned and returns it (it could just be size() - 1 if you don't care about gaps).
From an implementation standpoint, the code seems straightforward enough. I don't have much to suggest there other than 'de-inlining' your code.
Since it appears as though speed is one of the motivating factors for creating this, here's a small tip. Instead of:
inline void sort() const {
if (!sorted() && data_.size() > 0) {
...
}
Consider taking that initial 'if' out and don't inline this function. Put it outside of the class definition. You use sort for all kinds of read-only access cases but it's an exceptional case: most read access should be operating on already sorted data. When you have exceptional cases like this, your optimizing compiler will generally do a much better job if the function isn't inlined and you put your check outside:
inline const_reference operator[] (size_type i) const {
CHECK_LT(i, size_);
if (need_sort() )
sort();
...
}
Having done micro-optimizations on a daily basis and with the aid of a profiler, I can assure you that this will generally be faster on most optimizing compilers including latest versions of GCC, ICC, and MSVC. Roughly speaking, the reason is because your operator[] function will be doing less and accessing less memory as a result (no inlined sort to deal with which should only be executed in exceptional circumstances).
Most importantly, I recommend that you grab a profiler and do some tests and see where your bottlenecks are with this.
Related
Summary
I have a custom array class:
template<typename T, int SIZE>
class Array {
private:
T mArray[SIZE];
};
To enable support for std algorithms, with ranges, I want to create an iterator for this class. It would seem that std::contiguous_iterator would be the optimal choice since I can guarantee contiguous memory layout for the data. Following the iterator tutorial I should create a class inside this class. However, I should somehow be (quoted) "For example, instead of the std::forward_iterator_tag tag you would mark your iterator with the std::forward_iterator concept.".
I have a hard time figuring out what the syntax would look like for this, and I have been unable to find a post on the web showcasing this.
Question
How do I complete the following code snippet to implement std::contiguous_iterator for my Array<T,S> class?:
import <iterator>;
template<typename T, int SIZE>
class Array {
public:
const T& operator[](int i) { return mArray[i]; }
T& operator[](int i) { return mArray[i]; }
private:
T mArray[SIZE];
public:
struct Iterator {
Iterator(T* ptr) : mPtr(ptr) {}
private:
T* mPtr;
};
Iterator begin() { return Iterator(&mArray[0]); }
Iterator end() { return Iterator(&mArray[SIZE]); }
};
NOTE: There is a lot of operator overloads. An answer is not required to provide all of them. I just need an example syntax for where to place the concept, then I can probably figure out the rest.
As far as I could tell you need typedefs on the iterator class, so simply using a pointer was not sufficient. Here is an example:
#include <iterator>
#include <algorithm>
template<typename T, int SIZE>
class Array {
public:
const T& operator[](int i) const { return mArray[i]; }
T& operator[](int i) { return mArray[i]; }
private:
T mArray[SIZE];
public:
struct iterator
{
using difference_type=std::ptrdiff_t;
using value_type=std::remove_cv_t<T>;
using pointer=T*;
using reference=T&;
using iterator_category=std::random_access_iterator_tag;
using iterator_concept=std::contiguous_iterator_tag;
using self_type=iterator;
iterator(T *x) : ptr(x) {}
T operator*() { return *ptr; }
T operator->() { return ptr; }
difference_type operator-(const iterator& rhs) { return ptr-rhs.ptr; }
iterator& operator ++() { ++ptr; return *this;}
bool operator !=(const iterator& rhs) { return ptr != rhs.ptr; }
private:
T * ptr;
};
iterator begin() { return &mArray[0]; }
iterator end() { return &mArray[SIZE]; }
};
int foo(Array<int, 7>& a)
{
int sum;
for (auto x : a)
{
sum+=x;
}
return sum;
}
int goo(Array<int, 7>& a, int x)
{
auto ret=std::find(a.begin(), a.end(), x);
if (ret!=a.end()) return *ret;
return 0;
}
Note that you would likely need const_iterator and reverse_iterators for const and non-const ...
Credits
Thanks to #glenn-teitelbaum for pointing me in the right direction. I think I managed to figure out how to do this. It took a long time, so this will hopefully save someone else that trouble.
[Answering my own question]
The iterator should comply with the std::contiguous_iterator concept, so I looked at cppreference for the necessary parts. The concept is defined like this:
template<class I>
concept contiguous_iterator =
std::random_access_iterator<I> &&
std::derived_from</*ITER_CONCEPT*/<I>, std::contiguous_iterator_tag> &&
std::is_lvalue_reference_v<std::iter_reference_t<I>> &&
std::same_as<
std::iter_value_t<I>, std::remove_cvref_t<std::iter_reference_t<I>>
> &&
requires(const I& i) {
{ std::to_address(i) } ->
std::same_as<std::add_pointer_t<std::iter_reference_t<I>>>;
};
So in order to implement this concept, I must first also implement std::random_access_iterator, std::is_derived_from<[...]>, std::is_lvalue_reference_v<[...]>, and so on. This is a recursive process, especially since std::random_access_iterator builds on top of 4 other iterator types. Since this is a C++20 question, I aim to use C++20 features as much as possible (since it greatly simplifies the implementation). This is the complete iterator implementation:
template<typename T, int SIZE>
class Array
{
public:
class Iterator
{
public:
using iterator_category = std::contiguous_iterator_tag;
using iterator_concept = std::contiguous_iterator_tag;
//using difference_type = std::ptrdiff_t; // Likely the same
using difference_type = typename std::iterator<
std::contiguous_iterator_tag, T>::difference_type;
//using value_type = T;
using value_type = std::remove_cv_t<T>; // Using `T` seems sufficient
using pointer = T*;
using reference = T&;
// constructor for Array<T,S>::begin() and Array<T,S>::end()
Iterator(pointer ptr) : mPtr(ptr) {}
// std::weakly_incrementable<I>
Iterator& operator++() { ++mPtr; return *this; }
Iterator operator++(int) { Iterator tmp = *this; ++(*this); return tmp; }
Iterator() : mPtr(nullptr/*&mArray[0]*/) {} // TODO: Unsure which is correct!
// std::input_or_output_iterator<I>
reference operator*() { return *mPtr; }
// std::indirectly_readable<I>
friend reference operator*(const Iterator& it) { return *(it.mPtr); }
// std::input_iterator<I>
// No actions were needed here!
// std::forward_iterator<I>
// In C++20, 'operator==' implies 'operator!='
bool operator==(const Iterator& it) const { return mPtr == it.mPtr; }
// std::bidirectional_iterator<I>
Iterator& operator--() { --mPtr; return *this; }
Iterator operator--(int) { Iterator tmp = *this; --(*this); return tmp; }
// std::random_access_iterator<I>
// std::totally_ordered<I>
std::weak_ordering operator<=>(const Iterator& it) const {
return std::compare_three_way{}(mPtr, it.mPtr);
// alternatively: `return mPtr <=> it.mPtr;`
}
// std::sized_sentinel_for<I, I>
difference_type operator-(const Iterator& it) const { return mPtr - it.mPtr; }
// std::iter_difference<I> operators
Iterator& operator+=(difference_type diff) { mPtr += diff; return *this; }
Iterator& operator-=(difference_type diff) { mPtr -= diff; return *this; }
Iterator operator+(difference_type diff) const { return Iterator(mPtr + diff); }
Iterator operator-(difference_type diff) const { return Iterator(mPtr - diff); }
friend Iterator operator+(difference_type diff, const Iterator& it) {
return it + diff;
}
friend Iterator operator-(difference_type diff, const Iterator& it) {
return it - diff;
}
reference operator[](difference_type diff) const { return mPtr[diff]; }
// std::contiguous_iterator<I>
pointer operator->() const { return mPtr; }
using element_type = T;
private:
T* mPtr;
};
// === STATIC ASSERTS ===
// - to verify correct Iterator implementation!
static_assert(std::weakly_incrementable<Iterator>);
static_assert(std::input_or_output_iterator<Iterator>);
static_assert(std::indirectly_readable<Iterator>);
static_assert(std::input_iterator<Iterator>);
static_assert(std::incrementable<Iterator>);
static_assert(std::forward_iterator<Iterator>);
static_assert(std::bidirectional_iterator<Iterator>);
static_assert(std::totally_ordered<Iterator>);
static_assert(std::sized_sentinel_for<Iterator, Iterator>);
static_assert(std::random_access_iterator<Iterator>);
static_assert(std::is_lvalue_reference_v<std::iter_reference_t<Iterator>>);
static_assert(std::same_as<std::iter_value_t<Iterator>,
std::remove_cvref_t<std::iter_reference_t<Iterator>>>);
static_assert(std::contiguous_iterator<Iterator>);
const T& operator[](int i) const {
if (i < 0 || i >= SIZE) {
throw std::runtime_error("Array index out of bounds");
}
return mArray[i];
}
T& operator[](int i) {
if (i < 0 || i >= SIZE) {
throw std::runtime_error("Array index out of bounds");
}
return mArray[i];
}
Iterator begin() { return Iterator(&mArray[0]); }
Iterator end() { return Iterator(&mArray[SIZE]); }
private:
T mArray[SIZE];
};
// Check that the Array class can be used as a contiguous_range.
static_assert(std::ranges::contiguous_range<Array<int, 10>>);
NOTE: using element_type = T; was necessary because of a bug in the specification, which might be fixed. I found information about that here. Adding this fixed issue with std::to_address<Iterator> not being able to compile, and was the last missing piece in going from std::random_access_iterator to std::contiguous_iterator.
Testing
I did not perform a complete testing suite with all algorithms, but I chose a few which depend on ranges and std::random_access_iterator. It all runs smoothly. I also depend on building standard library headers as module units, because I want to showcase how C++20 features work together.
import <stdexcept>;
import <iostream>;
import <iterator>;
import <algorithm>;
import <random>;
#include <memory> // fails to build header unit!
template<typename T, int SIZE>
class Array
{
[...]
};
int main()
{
Array<int, 10> arr;
for (int i = 0; i < 10; i++) arr[i] = i;
// I need to call std::ragnes::shuffle since that depends on
// std::random_access_iterator, so that is a minimum.
// https://en.cppreference.com/w/cpp/algorithm/ranges/shuffle
std::random_device rd;
std::mt19937 gen{rd()};
std::cout << "before random shuffle:\n";
for (auto& i : arr) std::cout << i << ' ';
std::ranges::shuffle(arr, gen);
std::cout << "\nafter random shuffle:\n";
for (auto& i : arr) std::cout << i << ' ';
std::cout << '\n';
// Also std::ranges::stable_sort is a good check (also random_access_iterator):
// https://en.cppreference.com/w/cpp/algorithm/ranges/stable_sort
std::cout << "after stable_sort:\n";
std::ranges::stable_sort(arr);
for (auto& i : arr) std::cout << i << ' ';
std::cout << '\n';
auto [min,max] = std::ranges::minmax(arr);
std::cout << "min: " << min << ", max: " << max << '\n';
return 0;
}
I am encapsulating a std::list to make it safely iterable when iterating can potentially mark contents as 'invalid', and 'invalid' contents are skipped in the iteration. Specifically, during the iteration the current object could schedule itself or other objects for removal from the list and mark those objects as invalid. The list is then periodically cleaned of invalid objects.
How do I define an increment operator to make range-based for loops work correctly? Here is my class:
template <typename T> class DeferredCleanupList
{
public:
DeferredCleanupList() {
(void)static_cast<Valid *>((T)0);
}
virtual ~DeferredCleanupList() {}
typedef typename std::list<T>::iterator iterator;
iterator begin() {
iterator it = container.begin();
if ((*it)->valid())
return it;
return next(it);
}
iterator next(iterator it) {
do {
++it;
}
while (it != end() && !(*it)->valid());
return it;
}
iterator end() { return container.end(); }
// to be implemented:
// typedef typename std::list<T>::const_iterator const_iterator ;
// const_iterator cbegin() const { return container.cbegin(); }
// const_iterator cend() const { return container.cend(); }
// const_iterator cnext() const { ??? }
size_t size() const { return container.size(); }
void add(T *ptr) { container.push_front(ptr); }
void remove(T *ptr) { ptr->invalidate(); }
// called occasionally
void delete_invalid() {
for (auto it = container.begin(); it != container.end(); ) {
auto ptr = *it;
if (ptr->valid())
++it;
else {
delete ptr;
it = container.erase(it);
}
}
}
private:
DeferredCleanupList(const DeferredCleanupList&);
DeferredCleanupList& operator=(const DeferredCleanupList&);
std::list<T> container;
};
My current test case is something like:
int main() {
class D : public Valid {};
DeferredCleanupList<D *> list;
for (auto it = list.begin(); it != list.end(); it = list.next(it)); // works
for (auto ptr : list); // iterates, but doesn't call list.next(it)
}
EDIT:
After some trial and error, I wrote this iterator wrapper based on the suggestions in the comments:
template <typename T> class DeferredCleanupList
{
public:
class iterator {
public:
iterator(typename std::list<T>::iterator it, DeferredCleanupList<T>& ls) : it(it), list(ls) {}
iterator& operator=(const iterator& rhs) { it = rhs; return *this; }
iterator& operator++() {
do {
++it;
}
while (it != list.end().it && !(*it)->valid());
return *this;
}
friend bool operator==(const iterator& lhs, const iterator& rhs) { return lhs.it == rhs.it; }
friend bool operator!=(const iterator& lhs, const iterator& rhs) { return !(lhs == rhs); }
T& operator*() { return *it; }
private:
typename std::list<T>::iterator it;
DeferredCleanupList& list;
};
iterator begin() {
iterator it = iterator(container.begin(), *this);
if (it == end() || (*it)->valid())
return it;
return ++it;
}
iterator end() { return iterator(container.end(), *this); }
}
It appears to work perfectly in all the test cases I throw at it. Am I missing anything obvious with this approach? Is there a more elegant solution?
You can not do it within the container class. You should implement your own iterator and implement the behavior you want in it's prefix increment operator. You can do it several ways including inheriting one from STL or creating your own wrapper for std::list iterator.
Actually to implement your desired behavior in iterator you need to provide to your custom iterator class the end iterator of container.
I would not recommend you to implement your own iterators unless your design actually good with them. Unnecessary complications almost always lead to even more unnecessary complications.
I've modified your code so it actually compiles and implemented std::list iterator wrapper to behave as you planned.
#include <iostream>
#include <list>
#include <iterator>
using namespace std;
class Valid {};
template<typename _Tp>
struct myIteratorWrapper
{
typedef myIteratorWrapper<_Tp> _Self;
typedef _Tp value_type;
typedef _Tp* pointer;
typedef _Tp& reference;
typedef typename std::list<_Tp>::iterator listIterator;
listIterator it;
listIterator itEnd;
myIteratorWrapper(const listIterator& listIterArg) _GLIBCXX_NOEXCEPT
: it(listIterArg)
{}
myIteratorWrapper(const listIterator& itBegin,
const listIterator& itEnd) _GLIBCXX_NOEXCEPT
: it(itBegin), itEnd(itEnd)
{}
reference
operator*() const _GLIBCXX_NOEXCEPT
{ return *it; }
pointer
operator->() const _GLIBCXX_NOEXCEPT
{ return &(*it); }
/* Change logic of this method as you wish, but keep the signature */
_Self&
operator++() _GLIBCXX_NOEXCEPT
{
do
{
++it;
}while (it != itEnd && !(*it)->valid());
return *this;
}
bool
operator==(const _Self& __x) const _GLIBCXX_NOEXCEPT
{ return it == __x.it; }
bool
operator!=(const _Self& __x) const _GLIBCXX_NOEXCEPT
{ return it != __x.it; }
};
template <typename T> class DeferredCleanupList
{
public:
DeferredCleanupList() {
(void)static_cast<Valid *>((T)0);
}
virtual ~DeferredCleanupList() {}
typedef myIteratorWrapper<T> iterator;
iterator begin() {
iterator it(container.begin(), container.end());
return it;
}
iterator next(iterator it) {
return ++it;
}
iterator end() { return container.end(); }
size_t size() const { return container.size(); }
void add(T ptr) { container.push_front(ptr); }
void remove(T ptr) { ptr->invalidate(); }
// called occasionally
void delete_invalid() {
for (auto it = container.begin(); it != container.end(); ) {
auto ptr = *it;
if (ptr->valid())
++it;
else {
delete ptr;
it = container.erase(it);
}
}
}
private:
DeferredCleanupList(const DeferredCleanupList&);
DeferredCleanupList& operator=(const DeferredCleanupList&);
std::list<T> container;
};
class D : public Valid {
bool isValid;
std::string myName;
public:
D(std::string myName, bool arg = false)
: myName(myName), isValid(arg) {}
bool valid() const
{ return isValid; }
const std::string& whoAmI() const
{ return myName; }
};
int main() {
D o1("o1", true);
D o2("o2");
D o3("o3", true);
D o4("o4");
D o5("o5", true);
DeferredCleanupList<D *> list;
list.add(&o1);
list.add(&o2);
list.add(&o3);
list.add(&o4);
list.add(&o5);
for (auto ptr : list)
{
std::cout << ptr->whoAmI() << std::endl;
}
}
Output:
o5
o3
o1
From http://en.cppreference.com/w/cpp/language/range-for
The above syntax produces code equivalent to the following (__range,
__begin and __end are for exposition only):
{
auto && __range = range_expression ;
for (auto __begin = begin_expr, __end = end_expr;
__begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
ie you must have the preincrement operator defined.
I'm looking for a data structure that can address the following use-case:
Values are inserted from the back
The size of individual values is a few tens of bytes.
Values are naturally ordered in ascending order by one of their fields, which servers as a unique identifier.
Values are often removed from the front, but can also be removed from arbitrary locations specified by the key, so look-up and removal should be fast.
Copying a contiguous subset of the values to a new data-structure of this type should be cheap.
Clearing should be cheap.
Typically contains tens or hundreds of values, but thousands are also possible.
Performance should be consistent, as I'm using this for a soft real-time system.
I've been thinking along the lines of a deque + an auxiliary deque holding a bitmap which can be used to indicate the removal of values, but before I sit down to code I'd appreciate your advice. Thanks!
You can try linked unordered_map which will have a template arguments of key_type and node<value_type>, and that node will have previous\next values' keys.
The class will be somewhat similar to this: (meaning is incomplete)
#include <unordered_map>
template<typename key, typename value>
struct linked_map {
void push_back(key key_, value value_) {
if (!is_first_last_set)
first_last.first = key_;
assert(base.find(key_) == base.end());
base[key_] = value_;
// TODO: set prev/next_node_key
first_last.second = key_;
is_first_last_set = true;
}
void erase(key key_) {
// TODO: update previous and next node's previous and next keys
base.erase(base.find(key_));
}
value &front() {
return base[first_last.first].data;
}
void pop_front() {
erase(first_last.first);
}
...
bool is_first_last_set = false;
std::pair<key,key> first_last;
struct node {
value data;
key prev_node_key,next_node_key;
};
std::unordered_map<key,std::pair<key,value>> base;
};
The unordered_map is to make O(1) random-access for deletion.
The node in the value is to save the order in that unordered_map.
I have chose to use unordered_map, because it has more consistent performance than map as it doesn't have to allocate (allocation can take longer when memory is fragmented or for whatever reason) for every insertion, and the maximum the processor will have to do to insert\delete\front is a few cache misses.
I'd keep it simple.
To handle element deletion in the middle, I'd use optional<T>. Cleaner than a separate bit set. There is a modest memory cost compared to a separate bitset, but the memory being contiguous seems worth it.
As you have key-values, I'd go with a pair< key, optional<value> >, and I'd leave the key alone if I delete the value. This makes the searching code easier.
For double-ended ness, deque to start. It isn't ideal, but it is written. A double-ended circular vector could probably be made faster, but I am lazy.
I would write the data-structure as a class with appropriate methods. As a first pass, implement it as the simplest thing you can think of. (Probably a sorted vector).
Then you can go and implement the rest of the application, and then you can see if the data-structure is a bottleneck, and if so, optimize it. (By which point you will have a fully functioning application you can use to test different possible implementations.)
Having considered the suggestions, I opted for a deque with the following properties:
Intrusive: The values provide a key, by which they can be searched, and methods for marking them as deleted and for check whether they are deleted.
Sorted: The values are stored in ascending order of the key.
The front and back elements are always non-deleted.
I've pushed the code to GitHub - https://github.com/yitzikc/InstrusiveSortedDeque
And it's also available below:
/*
* InstrusiveSortedDeque.h
*
* Created on: Dec 4, 2016
* Author: yitzikc
*/
#ifndef UTILS_INTRUSIVESORTEDDEQUE_H_
#define UTILS_INTRUSIVESORTEDDEQUE_H_
#include <algorithm>
#include <deque>
#include <boost/iterator/filter_iterator.hpp>
namespace Utils {
// InstrusiveSortedDeque: A deque containing sorted values, which should be default constructible and supply the following types and methods:
// * typedef KeyType
// * KeyType GetKey() const;
// * IsDeleted() const; - indicating that a value should be considered as removed
// * Remove() - Designates a value as deleted.
template <typename T> // TODO: Add other template args allowing the allocator to be customized
class InstrusiveSortedDeque : public std::deque<T> {
private:
typedef std::deque<T> StdDeque;
static constexpr bool FilterPredicate(const T& value) { return ! value.IsDeleted(); }
// A default-constructible type which wraps FilterPredicate
struct FilterPredicateType {
inline bool operator() (const T& value)
{
return FilterPredicate(value);
}
};
public:
using typename StdDeque::allocator_type;
using typename StdDeque::size_type;
using typename StdDeque::pointer;
using typename StdDeque::const_pointer;
using typename StdDeque::reference;
using typename StdDeque::const_reference;
// A user-supplied key type
typedef typename T::KeyType key_type;
typedef T value_type;
// A key-type supporting quick access. Essentially a thin wrapper around indexes of the underlying deque class
class quick_key_type {
int m_index;
enum { INVALID_INDEX = -1, MIN_VALID_INDEX = 0 };
friend class InstrusiveSortedDeque;
explicit constexpr quick_key_type(int index = INVALID_INDEX)
: m_index(index)
{
}
public:
constexpr quick_key_type(const quick_key_type&) = default;
BOOST_CXX14_CONSTEXPR quick_key_type& operator=(const quick_key_type&) = default;
constexpr bool is_valid() const { return m_index >= MIN_VALID_INDEX; }
constexpr bool is_front() const { return m_index == MIN_VALID_INDEX; }
constexpr bool operator==(quick_key_type other) { return other.m_index == m_index; }
constexpr bool operator< (quick_key_type other) { return other.m_index < m_index; }
~quick_key_type() = default;
};
typedef boost::filter_iterator<FilterPredicateType, typename StdDeque::iterator> iterator;
typedef boost::filter_iterator<FilterPredicateType, typename StdDeque::const_iterator> const_iterator;
typedef boost::filter_iterator<FilterPredicateType, typename StdDeque::reverse_iterator> reverse_iterator;
typedef boost::filter_iterator<FilterPredicateType, typename StdDeque::const_reverse_iterator> const_reverse_iterator;
InstrusiveSortedDeque( const_iterator first, const_iterator last, const allocator_type& alloc = allocator_type() )
: StdDeque(first, last, alloc)
, m_nMarkedAsErased(0)
{
}
InstrusiveSortedDeque( iterator first, iterator last, const allocator_type& alloc = allocator_type() )
: StdDeque(first, last, alloc)
, m_nMarkedAsErased(0)
{
}
InstrusiveSortedDeque(const InstrusiveSortedDeque<T>& other)
: InstrusiveSortedDeque(other.cbegin(), other.cend(), other.get_allocator())
// FIXME: Aliasing the allocator might not be a good idea when we use custom allocators
{
}
template< class InputIt >
InstrusiveSortedDeque( InputIt first, InputIt last, const allocator_type& alloc = allocator_type() )
: StdDeque(MakeFilteredIter(this, first), MakeFilteredIter(this, last), alloc)
, m_nMarkedAsErased(0)
{
}
InstrusiveSortedDeque()
: StdDeque::deque()
, m_nMarkedAsErased(0)
{
}
using StdDeque::deque;
InstrusiveSortedDeque<T>& operator=(const InstrusiveSortedDeque<T>& other)
{
Clone(other);
return *this;
}
InstrusiveSortedDeque<T>& operator=(InstrusiveSortedDeque<T>&& other)
{
static_cast<StdDeque*>(this)->operator=(other);
m_nMarkedAsErased = other.m_nMarkedAsErased;
return *this;
}
// Accessors by quick_key
reference at(quick_key_type key)
{
return GetByQuickKey<reference>(this, key);
}
const_reference at(quick_key_type key) const
{
return GetByQuickKey<const_reference>(this, key);
}
reference operator[](quick_key_type key)
{
return GetByQuickKey<reference>(this, key);
}
const_reference operator[](quick_key_type key) const
{
return GetByQuickKey<const_reference>(this, key);
}
// Methods for obtaining forward iterators
iterator begin()
{
ValidateEdge(this->front());
return MakeFilteredIter(this, StdDeque::begin());
}
const_iterator begin() const
{
ValidateEdge(this->front());
return MakeFilteredIter(this, StdDeque::begin());
}
const_iterator cbegin() const
{
return begin();
}
iterator end()
{
ValidateEdge(this->back());
return MakeFilteredIter(this, StdDeque::end());
}
const_iterator end() const
{
ValidateEdge(this->back());
return MakeFilteredIter(this, StdDeque::end());
}
const_iterator cend() const
{
return end();
}
// Methods for obtaining reverse iterators
reverse_iterator rbegin()
{
ValidateEdge(this->back());
return MakeFilteredIter(this, StdDeque::rbegin());
}
const_reverse_iterator rbegin() const
{
ValidateEdge(this->back());
return MakeFilteredIter(this, StdDeque::rbegin());
}
const_reverse_iterator crbegin() const
{
return rbegin();
}
reverse_iterator rend()
{
ValidateEdge(this->front());
return MakeFilteredIter(this, StdDeque::rend());
}
const_reverse_iterator rend() const
{
ValidateEdge(this->front());
return MakeFilteredIter(this, StdDeque::rend());
}
const_reverse_iterator crend() const
{
return end();
}
iterator quick_key_to_iterator(quick_key_type qk)
{
return QuickKeyToIterator(this, qk);
}
const_iterator quick_key_to_iterator(quick_key_type qk) const
{
return QuickKeyToIterator(this, qk);
}
size_type size() const
{
const size_type baseSize = capacity();
if (baseSize <= 1) {
assert(0 == m_nMarkedAsErased);
return baseSize;
}
const ssize_t netSize = baseSize - m_nMarkedAsErased;
assert(netSize > 1);
return netSize;
}
// Size of the underlying deque
size_type capacity() const
{
return StdDeque::size();
}
// Find methods which return an iterator to the specified key using a binary search
const_iterator find(key_type k) const
{
typename StdDeque::const_iterator it;
DoFind(this, it, StdDeque::cbegin(), StdDeque::cend(), k);
return MakeFilteredIter(this, std::move(it));
}
// Find methods which return an iterator to the specified key using a binary search
iterator find(key_type k)
{
typename StdDeque::iterator it;
DoFind(this, it, StdDeque::begin(), StdDeque::end(), k);
return MakeFilteredIter(this, std::move(it));
}
// An alternate find, starts by searching at the front of the deque before trying the usual search,
// and returns a 'quick key' instead of an iterator
quick_key_type find_front(key_type userKey) const
{
if (! this->empty()) {
if (this->front().GetKey() == userKey) {
return quick_key_type(quick_key_type::MIN_VALID_INDEX);
}
else {
// TODO: cache the result that we find here, and try searching from the cached value.
typename StdDeque::const_iterator it;
if (DoFind(this, it, StdDeque::cbegin(), StdDeque::cend(), userKey)) {
return quick_key_type(it - StdDeque::cbegin());
}
}
}
return quick_key_type();
}
void erase(iterator& it)
{
erase(it.base());
return;
}
// TODO: Also implement the overload using a range.
bool erase(key_type k)
{
typename StdDeque::iterator it;
if (DoFind(this, it, StdDeque::begin(), StdDeque::end(), k)) {
return erase(it);
}
return false;
}
bool erase(quick_key_type k)
{
return erase(StdDeque::begin() + k.m_index);
}
void pop_front()
{
assert(! this->empty() && ! this->front().IsDeleted());
StdDeque::pop_front();
TrimFront();
}
void pop_back()
{
assert(! this->empty() && ! this->back().IsDeleted());
StdDeque::pop_back();
TrimBack();
}
void clear()
{
StdDeque::clear();
m_nMarkedAsErased = 0;
return;
}
void assign(const_iterator first, const_iterator last)
{
AssignFiltered(first, last);
}
void assign(iterator first, iterator last)
{
AssignFiltered(first, last);
}
template< class InputIt >
void assign( InputIt first, InputIt last )
{
AssignFiltered(MakeFilteredIter(this, first), MakeFilteredIter(this, last));
}
// Wrappers for emplace_back() and emplace_front(), which return references to the newly created values
// Note that this is incompatible with the C++ 17 interface, where emplace...() methods return iterators
// A more flexible emplace_back which will attempt to emplace at the back but will insert at the correct position
// if the new value is not in fact greater than the last value
template< typename... Args >
reference emplace_back(Args&&... args)
{
const_pointer const prevBack = this->empty() ? nullptr : & (this->back());
StdDeque::emplace_back(std::forward<Args>(args)...);
reference& back = this->back();
assert(! back.IsDeleted());
if (nullptr != prevBack) {
assert(! prevBack->IsDeleted());
if (BOOST_UNLIKELY(back.GetKey() <= prevBack->GetKey())) {
assert(back.GetKey() < prevBack->GetKey());
auto it = DoFindUnchecked(StdDeque::begin(), StdDeque::end() - 1, back.GetKey());
assert((it->GetKey() > back.GetKey()) && (& *it != &back));
auto newIt = StdDeque::emplace(it, std::move(back));
StdDeque::pop_back();
ValidateEdge(this->back());
return *newIt;
}
}
return back;
}
// TODO: Handle out-of-place values in emplace_front like in emplace_back
template< typename... Args >
reference emplace_front(Args&&... args)
{
const_pointer const prevFront = this->empty() ? nullptr : & (this->front());
StdDeque::emplace_front(std::forward<Args>(args)...);
assert(! this->front().IsDeleted());
assert( (nullptr == prevFront) ||
((! prevFront->IsDeleted()) && (this->front().GetKey() < prevFront->GetKey())));
return this->front();
}
// FIXME: Implement resize() to to maintain the invariants for m_nMarkedAsErased if it shrinks
// FIXME: Implement the other overloads of assign() to maintain the invariants for m_nMarkedAsErased
private:
typename StdDeque::size_type m_nMarkedAsErased = 0;
void TrimFront()
{
while (! this->empty() && this->front().IsDeleted()) {
StdDeque::pop_front();
--m_nMarkedAsErased;
}
return;
}
void TrimBack()
{
while (! this->empty() && this->back().IsDeleted()) {
StdDeque::pop_back();
--m_nMarkedAsErased;
}
return;
}
template <typename ThisType, typename IterType>
static bool DoFind(ThisType thisPtr, IterType& result, IterType&& beginIter, IterType&& endIter, key_type k)
{
if (! thisPtr->empty() && (k <= thisPtr->back().GetKey())) {
result = DoFindUnchecked(beginIter, endIter, k);
// FIXME: We should handle cases when endIter != StdDeque::end()
assert(result != thisPtr->StdDeque::end());
if ((result->GetKey() == k) && (endIter != result)) {
return true;
}
}
result = thisPtr->StdDeque::end();
return false;
}
template <typename IterType>
static inline auto DoFindUnchecked(IterType&& beginIter, IterType&& endIter, key_type k)
{
constexpr auto FindComp = [](const T& value, typename T::KeyType k)->bool { return value.GetKey() < k; };
return std::lower_bound(beginIter, endIter, k, FindComp);
}
void Clone(const InstrusiveSortedDeque& other)
{
StdDeque::resize(other.size()); // Pre-allocate space if necessary
constexpr auto pred = [](const value_type& r) { return ! r.IsDeleted(); };
std::copy_if(other.StdDeque::begin(), other.StdDeque::end(), StdDeque::begin(), pred);
m_nMarkedAsErased = 0;
}
template <typename RefType, typename ThisType>
static inline RefType GetByQuickKey(ThisType thisPtr, quick_key_type key)
{
return thisPtr->StdDeque::at(key.m_index);
}
template <typename ThisType>
static auto QuickKeyToIterator(ThisType thisPtr, const quick_key_type qk)
{
if (qk.is_valid()) {
auto it = thisPtr->StdDeque::begin() + qk.m_index;
if (! it->IsDeleted()) {
return MakeFilteredIter(thisPtr, std::move(it));
}
}
return MakeFilteredIter(thisPtr, thisPtr->StdDeque::end());
}
// Filter iterator wrapping implementation
template <typename ThisType, typename BaseIterType>
inline static boost::filter_iterator<FilterPredicateType, BaseIterType> MakeFilteredIter(ThisType thisPtr, BaseIterType&& baseIter)
{
return boost::make_filter_iterator<FilterPredicateType>(baseIter, thisPtr->StdDeque::end());
}
template< class InputIt >
void AssignFiltered(InputIt first, InputIt last)
{
StdDeque::assign(first, last);
m_nMarkedAsErased = 0;
}
// Validate that a value is a valid fron or back value
void ValidateEdge(const value_type& v) const
{
// Note that if the deque is empty, the reference to v might be garbage.
assert(this->empty() || ! v.IsDeleted());
}
bool erase(typename StdDeque::iterator it)
{
if (! it->IsDeleted()) {
it->Remove();
assert(it->IsDeleted());
++m_nMarkedAsErased;
TrimFront();
TrimBack();
return true;
}
else {
assert((capacity() > 1) && (m_nMarkedAsErased > 0));
ValidateEdge(this->front());
ValidateEdge(this->back());
return false;
}
}
};
} // namespace Utils
#endif /* UTILS_INTRUSIVESORTEDDEQUE_H_ */
I have a vector that looks like this:
std::vector<std::vector<MyClass>> myVector;
And I would like to access its elements through iterators as if it was an unidimensional vector:
for (auto& x : myVector)
{
foo(x); // x is an object of type MyClass
}
(i.e. the fact that there are multiple dimensions is transparent to whoever loops through myVector)
I have an idea of how this should be done, have a custom iterator implementation that saves current indexes so that when one of the vectors has no more elements, it resets one of the indexes and increments the other so that it can start iterating through the next vector and so on. But I have been trying to code this idea but can't seem to get this working. Does anyone have any idea of how I can possibly achieve this? Or even better, if there's any open-source project that has a similar implementation?
Thanks.
It's totally possible to define your own iterator to hide all the details of iterating through a vector of vector, I wrote some code to give you the idea, mind that it should require more checks but it basically works and give you the idea.
You just need to write the required operations to make it work in other code like an opaque iterator.
template <typename T>
struct vector2d
{
public:
class iterator
{
public:
using vector_type = std::vector<std::vector<T>>;
using first_level_iterator = typename std::vector<std::vector<T>>::iterator;
using second_level_iterator = typename std::vector<T>::iterator;
private:
vector_type& data;
first_level_iterator fit;
second_level_iterator sit;
public:
iterator(vector_type& data, bool begin) : data(data)
{
if (begin)
{
fit = data.begin();
sit = fit->begin();
}
else
{
fit = data.end();
}
}
inline bool operator!=(const iterator& other) const { return fit != other.fit || (fit != data.end() && sit != other.sit); }
inline const iterator& operator++() {
// don't go past end
if (fit == data.end())
return *this;
// increment inner iterator
++sit;
// if we reached the end of inner vector
if (sit == fit->end())
{
// go to next vector
++fit;
// if we reached end then don't reset sit since it would be UB
if (fit != data.end())
sit = fit->begin();
}
return *this;
}
T& operator*() const { return *sit; }
};
public:
std::vector<std::vector<T>> data;
iterator begin() { return iterator(this->data, true); }
iterator end() { return iterator(this->data, false); }
};
A small test:
int main() {
vector2d<int> data;
data.data.push_back(vector<int>());
data.data.push_back(vector<int>());
data.data.push_back(vector<int>());
for (int i = 1; i < 5; ++i)
{
data.data[0].push_back(i);
data.data[1].push_back(i*2);
data.data[2].push_back(i*3);
}
for (auto i : data)
{
cout << i << endl;
}
return 0;
}
The behavior is rather simple but you must make sure that it's always consistent for all the edge cases.
A pretty minimal range type:
template<class It>
struct range_t {
private:
It b, e;
public:
It begin() const { return b; }
It end() const { return e; }
decltype(auto) front() const { return *b; }
decltype(auto) back() const { return *std::prev(e); }
bool empty() const { return b==e; }
range_t without_front( std::size_t n = 1 ) const {
auto r = *this;
std::advance(r.b,n);
return r;
}
range_t without_back( std::size_t n = 1 ) const {
auto r = *this;
std::advance(r.e,std::ptrdiff_t(-n));
return r;
}
range_t(It s, It f):b(std::move(s)), e(std::move(f)) {}
range_t():b(), e() {}
};
template<class It>
range_t<It> range( It b, It e ) {
return {std::move(b), std::move(e)};
}
Doing this task is far easier with ranges than with iterators.
template<class Outer, class Inner>
struct stacked_range_t {
range_t<Outer> outer;
stacked_range_t()=default;
stacked_range_t( range_t<Outer> o ):outer(std::move(o)) {}
struct iterator {
private:
range_t<Outer> outer;
range_t<Inner> inner;
public:
iterator(
range_t<Outer> o,
range_t<Inner> i
):outer(std::move(o)), inner(std::move(i)) {}
iterator()=default;
friend auto mytie(iterator const& it) {
return std::tie( it.outer.begin(), it.inner.begin(), it.inner.end() );
}
friend bool operator==(iterator const& lhs, iterator const& rhs) {
return mytie(lhs)==mytie(rhs);
}
friend bool operator!=(iterator const& lhs, iterator const& rhs) {
return mytie(lhs)==mytie(rhs);
}
using difference_type = std::ptrdiff_t;
using value_type = typename std::iterator_traits<Inner>::value_type;
using pointer = typename std::iterator_traits<Inner>::pointer;
using reference = typename std::iterator_traits<Inner>::reference;
using iterator_category = std::input_iterator_tag;
reference operator*() const {
return *inner.begin();
}
pointer operator->() const {
return inner.begin().operator->();
}
iterator& operator++() {
using std::begin; using std::end;
inner = inner.without_front();
while (inner.empty())
{
outer = outer.without_front();
if (!outer.empty())
inner = range( begin(outer.front()), end(outer.front()) );
}
return *this;
}
iterator operator++(int) {
auto it = *this;
++*this;
return it;
}
};
iterator end() const {
return { range( outer.end(), outer.end() ), {} };
}
// a bit tricky:
iterator begin() const {
if (outer.empty()) return end();
auto rout = outer;
while( !rout.empty() ) {
using std::begin; using std::end;
auto rin = range( begin(rout.front()), end(rout.front()) );
if (!rin.empty())
return {std::move(rout), std::move(rin)};
rout = rout.without_front();
}
return end();
}
};
and a function to create it:
template<class Range>
auto make_stacked_range(Range& r) {
using std::begin; using std::end;
using Outer = decltype(begin(r));
using Inner = decltype(begin(*begin(r));
return stacked_range_t<Outer, Inner>{
{begin(r), end(r)}
};
}
there probably are typos. Use of C++1z features can be worked around with overly annoying decltype expressions and helper traits classes.
Relies on the iterators being trivially constructible, and such trivially constructed iterators are equal.
try to use template recursion,e.g.:
#include <stdio.h>
#include <vector>
template <typename V>
void f(V& v){
for(auto& e : v){
f(e);
}
printf("\n");
}
template <>
void f(int& v){
printf("%d ",v);
}
int main(){
std::vector<int> v1={1,2};
f(v1);
std::vector<std::vector<int> > v2={{3,4},{5,6,7}};
f(v2);
return 0;
};
With range/v3:
for (auto& x : myVector | ranges::view::join)
{
foo(x); // x is an object of type MyClass&
}
Demo
How do I create a custom class to loop over consecutive pairs of items in a STL container using a range-based loop?
This is the syntax and output I want:
std::list<int> number_list;
number_list.push_back(1);
number_list.push_back(2);
number_list.push_back(3);
auto paired_list = Paired(number_list);
for (const auto & pair : paired_list) {
std::printf("The pair is (%d, %d)\n", *(pair[0]), *(pair[1]));
// or
//std::printf("The pair is (%d, %d)\n", *(pair.first), *(pair.second));
}
// output:
// The pair is (1, 2)
// The pair is (2, 3)
I know these (and more) are needed, but I can't figure it out:
template <class T>
class Paired {
???
class iterator {
???
}
iterator begin() {
...
}
iterator end() {
...
}
}
Don't worry about const modifiers.
No boost.
Do not modify or copy objects in the container.
Here's what I would do.
#include <iterator>
#include <utility>
template <typename FwdIt> class adjacent_iterator {
public:
adjacent_iterator(FwdIt first, FwdIt last)
: m_first(first), m_next(first == last ? first : std::next(first)) { }
bool operator!=(const adjacent_iterator& other) const {
return m_next != other.m_next; // NOT m_first!
}
adjacent_iterator& operator++() {
++m_first;
++m_next;
return *this;
}
typedef typename std::iterator_traits<FwdIt>::reference Ref;
typedef std::pair<Ref, Ref> Pair;
Pair operator*() const {
return Pair(*m_first, *m_next); // NOT std::make_pair()!
}
private:
FwdIt m_first;
FwdIt m_next;
};
template <typename FwdIt> class adjacent_range {
public:
adjacent_range(FwdIt first, FwdIt last)
: m_first(first), m_last(last) { }
adjacent_iterator<FwdIt> begin() const {
return adjacent_iterator<FwdIt>(m_first, m_last);
}
adjacent_iterator<FwdIt> end() const {
return adjacent_iterator<FwdIt>(m_last, m_last);
}
private:
FwdIt m_first;
FwdIt m_last;
};
template <typename C> auto make_adjacent_range(C& c) -> adjacent_range<decltype(c.begin())> {
return adjacent_range<decltype(c.begin())>(c.begin(), c.end());
}
#include <iostream>
#include <vector>
using namespace std;
void test(const vector<int>& v) {
cout << "[ ";
for (const auto& p : make_adjacent_range(v)) {
cout << p.first << "/" << p.second << " ";
}
cout << "]" << endl;
}
int main() {
test({});
test({11});
test({22, 33});
test({44, 55, 66});
test({10, 20, 30, 40});
}
This prints:
[ ]
[ ]
[ 22/33 ]
[ 44/55 55/66 ]
[ 10/20 20/30 30/40 ]
Notes:
I haven't exhaustively tested this, but it respects forward iterators (because it doesn't try to use operations beyond ++, !=, and *).
range-for has extremely weak requirements; it doesn't require all of the things that forward iterators are supposed to provide. Therefore I have achieved range-for's requirements but no more.
The "NOT m_first" comment is related to how the end of the range is approached. An adjacent_iterator constructed from an empty range has m_first == m_next which is also == last. An adjacent_iterator constructed from a 1-element range has m_first pointing to the element and m_next == last. An adjacent_iterator constructed from a multi-element range has m_first and m_next pointing to consecutive valid elements. As it is incremented, eventually m_first will point to the final element and m_next will be last. What adjacent_range's end() returns is constructed from (m_last, m_last). For a totally empty range, this is physically identical to begin(). For 1+ element ranges, this is not physically identical to a begin() that has been incremented until we don't have a complete pair - such iterators have m_first pointing to the final element. But if we compare iterators based on their m_next, then we get correct semantics.
The "NOT std::make_pair()" comment is because make_pair() decays, while we actually want a pair of references. (I could have used decltype, but iterator_traits will tell us the answer too.)
The major remaining subtleties would revolve around banning rvalues as inputs to make_adjacent_range (such temporaries would not have their lives prolonged; the Committee is studying this issue), and playing an ADL dance to respect non-member begin/end, as well as built-in arrays. These exercises are left to the reader.
edit I was using transform.
Use adjacent_difference.
The second version takes a binary function which transforms the two values into a new
(different) value:
string make_message(int first, int second) {
ostringstream oss;
oss << "The pair is (" << first << ", " << second << ")";
return oss.str();
}
We can now transform the adjacent pairs into a third range. We'll use the ostream_iterator to use cout like a range:
list<int> numbers;
//...
adjacent_difference(numbers.begin(), numbers.end(),
ostream_iterator<string>(cout, "\n"),
make_message);
2nd edit
I found a question on comp.lang.c++.moderated asking why there aren't more 'adjacent' functions in the standard library such as for_each_adjacent. The reply said they were trivial to implement using std::mismatch.
I think this would be a better direction to go than implementing a special adjacent iterator.
Try this.
#include <list>
#include <iostream>
template<class T, class TIter = typename T::iterator, class TVal = typename T::value_type>
class PairedImpl {
T& m_t;
public:
class Iter {
TIter m_it;
public:
Iter(const TIter & it) : m_it(it) {}
bool operator!=(const Iter& it) { return m_it != it.m_it; }
Iter& operator++() { ++m_it; return *this; }
const Iter & operator *() const { return *this; }
const TVal & first() const { return *m_it; }
const TVal & second() const { return *std::next(m_it); }
};
PairedImpl(T& t) : m_t(t) {}
Iter begin() { return Iter(m_t.begin()); }
Iter end() {
TIter end = m_t.end();
return Iter(m_t.empty() ? end : --end);
}
};
template<class T>
PairedImpl<T> Paired(T& t) {
return PairedImpl<T>(t);
}
Usage
int main()
{
std::list<int> lst;
lst.push_back(1);
lst.push_back(2);
lst.push_back(3);
lst.push_back(4);
lst.push_back(5);
for (const auto & pair : Paired(lst)) {
std::cout << "(" << pair.first() << ", " << pair.second() << ")" << std::endl;
}
return 0;
}
Okay, an hour with no answers, I've come up with a solution which works. Note that this uses my own FixedLengthVector which is exactly what it sounds like.
template <class T>
class Grouped {
private:
// length of grouped objects
static const unsigned length_ = 2;
// hold pointer to base container to avoid comparing incompatible iterators
T * base_container_;
public:
// constructor
Grouped(T & base_container) :
base_container_(&base_container) {
}
// iterator
class iterator {
private:
// hold pointer to base container to avoid comparing incompatible iterators
T * base_container_;
// hold pointers to objects in base container
FixedLengthVector<length_, typename T::value_type *> ptr_;
// hold iterator to last object
typename T::iterator last_iterator_;
public:
// constructor
iterator(T & base_container, typename T::iterator & it)
: base_container_(&base_container),
last_iterator_(it) {
// set up pointers if possible
unsigned i = 0;
// check for end iterator
if (last_iterator_ == base_container_->end()) {
ptr_.fill(NULL);
return;
}
// set up first object
ptr_[0] = &*last_iterator_;
// set up next objects
for (unsigned i = 1; i < length_; ++i) {
++last_iterator_;
if (last_iterator_ == base_container_->end()) {
ptr_.fill(NULL);
return;
}
ptr_[i] = &*last_iterator_;
}
}
// dereference operator
FixedLengthVector<length_, typename T::value_type *> & operator * (void) {
assert(ptr_[0] != NULL);
return ptr_;
}
// pre-increment
iterator & operator ++ (void) {
// can't increase past end
assert(last_iterator_ != base_container_->end());
// find next iterator
++last_iterator_;
if (last_iterator_ == base_container_->end()) {
ptr_.fill(NULL);
return * this;
}
// cycle pointers left
for (unsigned i = 1; i < length_; ++i) {
ptr_[i - 1] = ptr_[i];
}
ptr_[length_ - 1] = &*last_iterator_;
return * this;
}
// equality comparison
bool operator == (const iterator & that) const {
return base_container_ == that.base_container_ &&
last_iterator_ == that.last_iterator_;
}
// inequality comparison
bool operator != (const iterator & that) const {
return !(*this == that);
}
};
// end iterator
iterator end() {
return iterator(*base_container_, base_container_->end());
}
// begin iterator
iterator begin() {
return iterator(*base_container_, base_container_->begin());
}
};