overridden method not working on the range_expression of a range based loop

Viewed 85

The first call to operator* (live example line 38) is doing what is expected, it calls the overridden method operator_deref(). Whereas the second (live example line 40) is calling the parent method instead. What I'm missing here?

Live example

#include <iostream>

class RangeP{
protected:
    int _iter = 0, _last;
    virtual int operator_deref() const{
        std::cout << "Parent Deref called" << std::endl;
        return _iter; 
    }

public:
    RangeP(int last_) : _last(last_) {}
    // Iterable functions
    const RangeP& begin() const { return *this; }
    const RangeP& end()   const { return *this; }
    // Iterator functions
    bool operator!=(const RangeP&) const { return _iter != _last; }
    void operator++() { ++_iter; }
    int operator*() {  
        return operator_deref(); 
    }
};

class RangeC : public RangeP{
    int operator_deref() const override{ // Enhanced operator
        std::cout << "Child Deref called" << std::endl;
        return 0;
    }

public:
    RangeC(int last_) : RangeP(last_) {};

};


int main(){
    auto a = RangeC(1);
    auto b = *a;               // calls the overridden method as expected

    for(auto i : RangeC(1)){}  // calls the parent method (Why?)  
}   

/* Output *

Child Deref called
Parent Deref called

*/

Thank you very much in advance.

2 Answers

The reason for it can be found at https://en.cppreference.com/w/cpp/language/range-for

As stated:

{
auto && __range = range_expression ;
for (auto __begin = begin_expr, __end = end_expr; __begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}

What you see here is that __begin is a copy of the begin_expr. So you end up writing auto __begin = __range.begin();

If it would have had something like auto &&, it could uphold the reference. Now you are experiencing something we call slicing. That's the copying of an abstract object where the original object is a derived class and the copy is of the base type.

A way to solve this is by creating a separate iterator class that behaves as a proxy.

EDIT

class RangeP {
 protected:
  int _last;
  virtual int operator_deref(int iter) const {
    std::cout << "Parent Deref called" << std::endl;
    return iter;
  }

 public:
  class const_iterator final {
   public:
    const_iterator(const RangeP &range, int iter) : _range{range}, _iter{iter} {}
    bool operator!=(const const_iterator &rhs) const { return _iter != rhs._iter; }
    void operator++() { ++_iter; }
    int operator*() { return _range.operator_deref(_iter); }
    const RangeP &_range;
    int _iter;
  };

  RangeP(int last_) : _last(last_) {}
  // Iterable functions
  auto begin() const { return const_iterator{*this, 0}; }
  auto end() const { return const_iterator{*this, _last}; }
};

class RangeC : public RangeP {
  int operator_deref(int iter) const override {  // Enhanced operator
    std::cout << "Child Deref called" << std::endl;
    return 0;
  }

 public:
  RangeC(int last_) : RangeP(last_){};
};

Since I didn't want to mess with const_cast to be able to call the operator++ on the RangeP class, I rewrote your code.

As you can see, RangeP now has an inner class called const_iterator. This can be copied without slicing and has the link to the range class where I can call operator_deref.

A second change I made, was passing the current iterator number to the operator_deref method, as this doesn't really belong in the range. The advantage of this is that multiple threads can loop over the same range without any race conditions and this more clearly indicates the intend. Code at compiler explorer

Range-based for-loops use the begin/end member functions which are not polymorphic in your class definition.


Notes: Some incorrect assertions have been made in the comments:

If you change the definition of the derived class to:

class RangeC : public RangeP{
    public:
    int operator_deref() const override{ // Enhanced operator
        std::cout << "Child Deref called" << std::endl;
        return 0;
    }

public:
    RangeC(int last_) : RangeP(last_) {};
    virtual const RangeC& begin() const { return *this; }
    virtual const RangeC& end()   const { return *this; }

};

You get the expected output:

Child Deref called
Child Deref called
Related