5

Suppose I have some array-based code that is enabled to be used by expression templates. E.g., I have operator[] for these arrays overloaded and also have overloaded the arithmetic operator + etc.

Now I would like to let the STL algorithm any_of run on such arrays. The simple way is to do

ExprArray<double, N> b, c; // init etc. 
auto a = b + c;            // for (auto i = 0; i < N; ++i) { a[i] = b[i] + c[i]; }
auto res = std::any_of(begin(a), end(a), SomePred{});

Of course, I would like to be able to short-circuit the computation and have a modified (range-based) lib::any_of that does

// only compute b[i] + c[i] until SomePred is satisified
auto res = lib::any_of(b + c, SomePred{}); // write as explicit loop over b[i] + c[i]

Writing lib::any_of in terms of operator[] on its input will do that job, the same as it was done for the overloaded operator+. However, this would require similar reimplementations of all STL algorithms that I could possibly run on such arrays.

Question: So suppose I want to re-use existing range-based algorithms (Boost.Range, range-v3) by only modifying the ExprArray iterators. Is it possible to modify the ExprArray iterator operator* and operator++ in such a way that this is transparent to range-based algorithms?

// only compute b[i] + c[i] until SomePred is satisified
// needs to eventually dispatch to 
// for (auto i = 0; i < N; ++i)
//     if (SomePred(b[i] + c[i])) return true;
// return false;
auto res = ranges::any_of(b + c, SomePred{});

So if the algorithm version actually is implemented in terms of iterators, the loop for (auto it = first; it != last; ++it) needs *it to be aware of the fact that it needs to compute b[i] + c[i], and ++it has to know that it needs to do ++i.

Ivaylo Strandjev
  • 69,226
  • 18
  • 123
  • 176
TemplateRex
  • 69,038
  • 19
  • 164
  • 304
  • Stopping when the predicate is true should be a natural thing to do for `std::any_of`, have you checked that it doesn't already does that? (You can do that by passing a lambda that prints the values it receives as the predicate.) – Some programmer dude Aug 31 '15 at 11:33
  • 1
    @JoachimPileborg the stopping is not really the problem, it's the lazy computation of the expression template and its begin/end iterators. If I only have overloaded `operator[]`, I need an iterator based implementation to be aware of the fact that it receives an expression template and to translate `*it` to the appropriate `expr[i]`. The question is, can `operator*` of the iterator do that kind of work for me? – TemplateRex Aug 31 '15 at 11:42
  • @TemplateRex please note that the question is about algorithm c++ library not about algorithm design, so the algorithm tag is inappropriate – Ivaylo Strandjev Aug 31 '15 at 11:44
  • @IvayloStrandjev thanks, I wasn't aware of this somewhat narrow scope. – TemplateRex Aug 31 '15 at 11:46
  • 1
    Since you're the one who writes the iterator class (I assume), you can have it do pretty much anything you need. – Angew is no longer proud of SO Aug 31 '15 at 11:50
  • @Angew true, but it needs to poke in the dark at the algorithm to know which kind of iterator expressions are actually used in the implementation. E.g. to translate the iterator based loop to an index-based loop etc. – TemplateRex Aug 31 '15 at 11:52
  • 1
    @TemplateRex Not at the *implementation* of the algorithm, but at the *requirements* for the iterator categories, I'd say. That is, what expressions are allowed on an iterator according to category. – Angew is no longer proud of SO Aug 31 '15 at 11:55

2 Answers2

4

This question seems to reduce to "Can I implement iterators for my expression templates?" which I think is fairly straightforward. Assuming that "expression templates" know their size and have overloaded operator[] an iterator simply needs to hold a reference to the expression object and an offset into the range it represents:

template <class Expr>
class iterator {
public:
  using iterator_category = ranges::random_access_iterator_tag;
  using difference_type = std::ptrdiff_t;
  using value_type = typename Expr::value_type;

  iterator() = default;
  constexpr iterator(Expr& e, difference_type i) :
    expr_{&e}, i_{i} {}

  constexpr bool operator==(const iterator& that) const {
    return assert(expr_ == that.expr_), i_ == that.i_;
  }
  constexpr bool operator!=(const iterator& that) const {
    return !(*this == that);
  }
  // Similarly for operators <, >, <=, >=

  value_type operator*() const {
    return (*expr_)[i_];
  }
  value_type operator[](difference_type n) const {
    return (*expr_)[i_ + n];

  iterator& operator++() & { ++i_; }
  iterator operator++(int) & { auto tmp = *this; ++*this; return tmp; }
  // Similarly for operator--

  iterator operator+(difference_type n) const {
    return iterator{expr_, i_ + n};
  }
  // Similarly for operators -, +=, and -=

  friend iterator operator+(difference_type n, const iterator& i) {
    return i + n;
  }

private:
    Expr* expr_;
    difference_type i_;
};

Now you simply have to arrange for "Expression templates" to have begin and end members that return iterator{*this, 0} and iterator{*this, size()}.

Casey
  • 41,449
  • 7
  • 95
  • 125
  • thanks, it even works for STL two-legged iterator algorithms, because `end()` does not have to access `operator[]` and can simply use the array size `N`. I was worried I had to compute the expression twice for both `begin` and `end`, but a range-wrapper will just store the expression template and dispatch both iterators. – TemplateRex Aug 31 '15 at 19:07
2

The question here is what b+c returns. If it returns a real ExprArray, you can't really have lazy evaluation. The array needs to be filled. You can't store references to b and c since you have no idea about their lifetime.

However, if it returns a LazyAddition whose conversion to ExprArray performs the addition, then it's trivial to see that LazyAddition::iterator can implement lazy addition as well. The risk here is auto a = b+c - this will create a LazyAddition object with pending references, not an ExprArray object.

Things get really nasty if you try to implement the ExprArray as a smart pointer behind the scenes. Sure, you could implement Copy-On-Write so that b+c is an ExprArray which keeps pointer to both original arrays. But as soon as you call T& ExprArray<T>::operator[] the COW will kick in, and copy the whole array on a single element read ! (C++ operator overloading rules on const do not work well for operator[], the const version gets selected when the argument itself is const, not when used for read access)

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • Re the lifetime issue: when calling `ranges::any_of(b+c, Pred{})` just assume that `b` and `c` are alive for the duration of the algorithm. – TemplateRex Aug 31 '15 at 11:56
  • "Just assume" is why I said there is a risk. You can't distinguish between `b+c` and `b+c`, even if one occurs in `ranges::any_of(b+c, Pred{})` and the other in `auto a = b+c`. it's up to you to decide if the risk is acceptable. – MSalters Aug 31 '15 at 11:59
  • I am using the example from Vandevoorde & Josuttis (ch 18 in their Templates book), where `operator=` copies alle elements from the rhs. The expression templates in that example all store the lhs and rhs arguments by reference in order to lazily compute the entire expression tree. So `b+c` is a lazy expression and accessing `operator[]` will only compute a single element, unless of course there is aliasing and `c` is a shifted version of `b`. – TemplateRex Aug 31 '15 at 12:05
  • BTW, in the V&J example, they have a `template ExprArray` template where `Rep` can indeed be a `LazyAddition` class. So `b+c` and similar expressions really are all `ExprArray`s, and there is no danger of having dangling references as long as you stay in local scope when passing expressions to algorithms. – TemplateRex Aug 31 '15 at 12:17