This code implements a simple wrapper, intended to be used with range-for to manipulate items of a container as overlapping pairs, ie. 3 items will give 2 pairs.
Code review focus: Is pair_range good modern C++? Any easily avoidable performance issues, like unnecessary copies?
#include <iostream>
#include <typeinfo>
#include <iterator>
#include <optional>
#include <utility>
template<class V, template<class...> class C>
class pair_range {
public:
struct pair_iterator {
using iterator_category = std::forward_iterator_tag;
using difference_type = ssize_t;
using value_type = std::pair<V, V>;
using pointer = ssize_t;
using reference = std::pair<V, V>; // no actual reference available
pair_iterator(const pair_range & sequence, pointer index = 0)
: m_sequence(sequence)
, m_index(index) {
};
const reference operator*() const {
return value_type{m_sequence.at(m_index), m_sequence.at(m_index+1)};
}
//operator->() not possible because pairs are not lvalues
// prefix increment
auto &operator++() {
m_index++;
return *this;
}
// postfix increment
auto operator++(int) {
auto tmp = *this;
++(*this);
return tmp;
}
friend bool operator== (const pair_iterator &a, const pair_iterator &b) {
return a.m_index == b.m_index && a.m_sequence.has_same_container(b.m_sequence);
};
friend bool operator!= (const pair_iterator &a, const pair_iterator &b) {
return a.m_index != b.m_index || !a.m_sequence.has_same_container(b.m_sequence);
};
private:
const pair_range &m_sequence;
pointer m_index = 0;
};
pair_range(const C<V> &source) : m_source_ptr(&source) {
const std::type_info &ti = typeid(*this);
std::cout << "&-constructor, type=" << ti.name() << std::endl;
}
pair_range(const C<V> &&source) : m_source_copy(std::move(source)), m_source_ptr(&m_source_copy.value()) {
const std::type_info &ti = typeid(*this);
std::cout << "&&-constructor, type=" << ti.name() << std::endl;
}
pair_iterator begin() const {
return pair_iterator{*this};
}
pair_iterator end() const {
auto size = m_source_ptr->size();
auto end = static_cast<typename pair_iterator::pointer>(size == 0 ? 0 : size - 1);
return pair_iterator{*this, end};
}
bool has_same_container(const pair_range &other) const {
return m_source_ptr == other.m_source_ptr;
}
private:
const V &at(typename pair_iterator::pointer index) const {
return (*m_source_ptr)[index];
}
const std::optional<const C<V> > m_source_copy;
const C<V> * const m_source_ptr = nullptr;
};
// test code
#include <algorithm>
#include <map>
#include <string>
#include <vector>
#include <variant>
template <class T>
void printPair(const T &pair)
{
const std::type_info &ti = typeid(pair);
std::cout << std::get<0>(pair) << ' ' << std::get<1>(pair)
<< ", type=" << ti.name() << std::endl;
}
int main()
{
std::cout << "--- empty list ---\n";
const std::vector<int> list1{};
for(auto pair : pair_range(list1)) {
printPair(pair);
}
std::cout << "--- 1 elements ---\n";
for(auto &pair : pair_range( std::vector{1} )) {
printPair(pair);
}
std::cout << "--- 2 elements ---\n";
std::vector xy{ std::string("x"), std::string("y") };
for(auto &pair : pair_range(xy)) {
printPair(pair);
}
std::cout << "--- 4 elements ---\n";
const std::string abcd = {"abcd"};
for(const std::tuple<char, char> &pair : pair_range(abcd)) {
printPair(pair);
}
}
Here's the output of that code on my machine:
--- empty list ---
&-constructor, type=10pair_rangeIiSt6vectorE
--- 1 elements ---
&&-constructor, type=10pair_rangeIiSt6vectorE
--- 2 elements ---
&-constructor, type=10pair_rangeINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt6vectorE
x y, type=St4pairINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES5_E
--- 4 elements ---
&-constructor, type=10pair_rangeIcNSt7__cxx1112basic_stringEE
a b, type=St5tupleIJccEE
b c, type=St5tupleIJccEE
c d, type=St5tupleIJccEE