C++ - Iterate over vector of doubles as tuples

Viewed 582

I have a C++ vector of doubles, which is guaranteed to have an even number of elements. This vector stores the coordinates of a set of points as x, y coordinates:

A[2 * i ] is the x coordinate of the i'th point.

A[2 * i + 1] is the y coordinate of the i'th point.

How to implement an iterator that allows me to use STL style algorithms (one that takes an iterator range, where dereferencing an iterator gives back the pair of doubles corresponding to the x, y coordinates of the corresponding point) ?

I'm using C++17 if that helps.

5 Answers

We can use range-v3, with two adapters:

  • chunk(n) takes a range and adapts it into a range of non-overlapping ranges of size n
  • transform(f) takes a range of x and adapts it into a range of f(x)

Putting those together we can create an adaptor which takes a range and yields a range of non-overlapping pairs:

auto into_pairs = rv::chunk(2)
                | rv::transform([](auto&& r){ return std::pair(r[0], r[1]); });

Using the r[0] syntax assumes that the input range is random-access, which in this case is fine since we know we want to use it on a vector, but it can also be generalized to work for forward-only ranges at the cost of a bit more syntax:

| rv::transform([](auto&& r){
      auto it = ranges::begin(r);
      auto next = ranges::next(it);
      return std::pair(*it, *next);
  })

Demo, using fmt for convenient printing:

int main() {
    std::vector<int> v = {1, 1, 2, 2, 3, 3, 4, 4, 5, 5};

    auto into_pairs = rv::chunk(2)
                    | rv::transform([](auto&& r){ return std::pair(r[0], r[1]); });

    // prints {(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)}
    fmt::print("{}\n", v | into_pairs);
}

It's unclear from the question if you wanted pair<T, T>s or pair<T&, T&>s. The latter is doable by providing explicit types to std::pair rather than relying on class template argument deduction.

C++ is a bit of a moving target - also when it comes to iterators (c++20 has concepts for that...). But would it not be nice to have a lazy solution to the problem. I.e. The tuples get generated on the fly, without casting (see other answers) and without having to write a loop to transform a vector<double> to a vector<tuple<double,double>>?

Now I feel I need a disclaimer because I am not sure this is entirely correct (language lawyers will hopefully point out, if I missed something). But it compiles and produces the expected output. That is something, yes?! Yes.

The idea is to build a pseudo container (which actually is just a facade to an underlying container) with an iterator of its own, producing the desired output type on the fly.

#include <vector>
#include <tuple>
#include <iostream>
#include <iterator>

template <class SourceIter>
struct PairWise {
  PairWise() = delete;
  PairWise(SourceIter first, SourceIter last) 
    : first{first}
    , last{last}
  {
  }
  using value_type = 
    typename std::tuple<
      typename SourceIter::value_type, 
      typename SourceIter::value_type
        >; 
  using source_iter = SourceIter;
  struct IterState {
    PairWise::source_iter first;
    PairWise::source_iter last;
    PairWise::source_iter current;
    IterState(PairWise::source_iter first, PairWise::source_iter last)
      : first{first}
      , last{last}
      , current{first}
    {
    }
    friend bool operator==(const IterState& a, const IterState& b) {
      // std::cout << "operator==(a,b)" << std::endl;
      return (a.first == b.first) 
        && (a.last == b.last)
        && (a.current == b.current);
    }
    IterState& operator++() {
      // std::cout << "operator++()" << std::endl;
      if (std::distance(current,last) >= 2) {
        current++;
        current++;
      }
      return *this;
    }
    const PairWise::value_type operator*() const {
      // std::cout << "operator*()" << std::endl;
      return std::make_tuple(*current, *(current+1));
    }
  };
  using iterator = IterState;
  using const_iterator = const IterState;
  const_iterator cbegin() const {
    return IterState{first,last};
  }
  const_iterator cend() const {
    auto i = IterState{first,last};
    i.current = last;    
    return i;
  }
  
  const_iterator begin() const {
    // std::cout << "begin()" << std::endl;
    return IterState{first,last};
  }
  const_iterator end() const {
    // std::cout << "end()" << std::endl;
    auto i = IterState{first,last};
    i.current = last;    
    return i;
  }
  source_iter first;
  source_iter last;
};

std::ostream& operator<<(std::ostream& os, const std::tuple<double,double>& value) {
  auto [a,b] = value;
  os << "<" << a << "," << b << ">";
  return os;
}

template <class Container>
auto pairwise( const Container& container) 
  -> PairWise<typename Container::const_iterator> 
{
  return PairWise(container.cbegin(), container.cend());
}

int main( int argc, const char* argv[]) {
  using VecF64_t = std::vector<double>;

  VecF64_t data{ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 };
  for (const auto x : pairwise(data)) {
    std::cout << x << std::endl;
  }
  
  return 0;
}

Elements in vector are stored in contiguous memory area, so you can use simple pointer arithmetics to access pair of doubles.

operator++ for iterator should skip 2 doubles at every use.

operator* can return tuple of references to double values, so you can read (pair = *it) or edit values (*it = pair).

struct Cont {
    std::vector<double>& v;
    Cont(std::vector<double>& v) : v(v) {}

    struct Iterator : public std::iterator<std::input_iterator_tag , std::pair<double,double>> {
        double* ptrData = nullptr;

        Iterator(double* data) : ptrData(data) {}

        Iterator& operator++() { ptrData += 2; return *this; }
        Iterator operator++(int) { Iterator copy(*this); ptrData += 2; return copy; }
        auto operator*() { return std::tie(*ptrData,*(ptrData+1)); }
        bool operator!=(const Iterator& other) const { return ptrData != other.ptrData; }
    };

    auto begin() { return Iterator(v.data()); }
    auto end() { return Iterator(v.data()+v.size());}
};


int main() {
    std::vector<double> v;
    v.resize(4);

    Cont c(v);
    for (auto it = c.begin(); it != c.end(); it++) {
        *it = std::tuple<double,double>(20,30);
    }
    std::cout << v[0] << std::endl; // 20
    std::cout << v[1] << std::endl; // 30
}

Demo

There's not an easy "C++" way to do this that's clean and avoids a copy of the original array. There's always this (make a copy):

vector<double> A; // your original list of points
vector<pair<double,double>> points;

for (size_t i = 0; i < A.size()/2; i+= 2)
{
    points[i*2] = pair<double,double>(A[i], A[i+1]);
}

The following would likely work, violates a few standards, and the language lawyers will sue me in court for suggesting it. But if we can assume that the sizeof(XY) is the size of two doubles, has no padding, and expected alignment then cheating with a cast will likely work. This assumes you don't need a std::pair

Non standard stuff ahead

vector<double> A; // your original list of points

struct XY {
   double x;
   double y;
};

static_assert(sizeof(double)*2 == sizeof(XY));

static_assert(alignof(double) == alignof(XY));

XY* points = reinterpret_cast<XY*>(A.data());
size_t numPoints = A.size()/2;

// iterate
for (size_t i = 0; i < numPoints; i++) {

     XY& point = points[i];

     cout << point.x << "," << point.y << endl;
}

If you can guarantee that std::vector will always have an even number of entries, you could exploit the fact that an vector of doubles will have the same memory layout as a vector of pairs of doubles. This is kind of a dirty trick though so I wouldn't recommend it if you can avoid it. The good news is that the standard guarantees that vector elements will be contiguous.

inline const std::vector<std::pair<double, double>>& make_dbl_pair(std::vector<double>& v)
{
  return reinterpret_cast<std::vector<std::pair<double, double>>&>(v);
}

This will only work for iteration with iterators. The size is likely to be double the number of pairs in the vector because it is still a vector of doubles underneath.

Example:

int main(int argc, char* argv[])
{
  std::vector<double> dbl_vec = { 0.0, 1.1, 2.2, 3.3, 4.4, 5.5 };
  const std::vector<std::pair<double, double>>& pair_vec = make_dbl_pair(dbl_vec);
  for (auto it = pair_vec.begin(); it != pair_vec.end(); ++it) {
    std::cout << it->first << ", " << it->second << "\n";
  }
  std::cout << "Size: " << dbl_vec.size() << "\n";
  return 0;
}
Related