1

The following code is based on that found in Modern C++ programming cookbook, and is compiled in VS 2017:

#include <iostream>

using namespace std;

template <typename T, size_t const Size> 
class dummy_array 
{ 
    T data[Size] = {}; 

public: 
    T const & GetAt(size_t const index) const 
    { 
        if (index < Size) return data[index]; 
        throw std::out_of_range("index out of range"); 
    } 

    // I have added this
    T & GetAt(size_t const index) 
    { 
        if (index < Size) return data[index]; 
        throw std::out_of_range("index out of range"); 
    } 

    void SetAt(size_t const index, T const & value) 
    { 
        if (index < Size) data[index] = value; 
        else throw std::out_of_range("index out of range"); 
    } 

    size_t GetSize() const { return Size; } 
};

template <typename T, typename C, size_t const Size> 
class dummy_array_iterator_type 
{ 
public: 
    dummy_array_iterator_type(C& collection,  
        size_t const index) : 
        index(index), collection(collection) 
    { } 

    bool operator!= (dummy_array_iterator_type const & other) const 
    { 
        return index != other.index; 
    } 

    T const & operator* () const 
    { 
        return collection.GetAt(index); 
    }

    // I have added this
    T & operator* () 
    { 
        return collection.GetAt(index); 
    } 

    dummy_array_iterator_type const & operator++ () 
    { 
        ++index; 
        return *this; 
    } 

private: 
    size_t   index; 
    C&       collection; 
};

template <typename T, size_t const Size> 
using dummy_array_iterator =  dummy_array_iterator_type<T, dummy_array<T, Size>, Size>; 

// I have added the const in 'const dummy_array_iterator_type'
template <typename T, size_t const Size> 
using dummy_array_const_iterator =  const dummy_array_iterator_type<T, dummy_array<T, Size> const, Size>;

template <typename T, size_t const Size> 
inline dummy_array_iterator<T, Size> begin(dummy_array<T, Size>& collection) 
{ 
    return dummy_array_iterator<T, Size>(collection, 0); 
} 

template <typename T, size_t const Size> 
inline dummy_array_iterator<T, Size> end(dummy_array<T, Size>& collection) 
{ 
    return dummy_array_iterator<T, Size>(collection, collection.GetSize()); 
} 

template <typename T, size_t const Size> 
inline dummy_array_const_iterator<T, Size> begin(dummy_array<T, Size> const & collection) 
{ 
    return dummy_array_const_iterator<T, Size>(collection, 0); 
} 

template <typename T, size_t const Size> 
inline dummy_array_const_iterator<T, Size> end(dummy_array<T, Size> const & collection) 
{ 
    return dummy_array_const_iterator<T, Size>(collection, collection.GetSize()); 
}

int main(int nArgc, char** argv)
{
    dummy_array<int, 10> arr;

    for (auto&& e : arr) 
    { 
        std::cout << e << std::endl; 
        e = 100;    // PROBLEM
    } 

    const dummy_array<int, 10> arr2;

    for (auto&& e : arr2)   // ERROR HERE
    { 
        std::cout << e << std::endl; 
    } 
}

Now, the error is pointing at the line

T & operator* ()

stating

'return': cannot convert from 'const T' to 'T &'"

...which is raised from my range based for loop on arr2.

Why is the compiler choosing the none-constant version of operator*()?. I have looked at this for a long time; I think its because it thinks that the object on which it is calling this operator is not constant: this should be a dummy_array_const_iterator. But, this object has been declared to be constant via

template <typename T, size_t const Size> 
using dummy_array_const_iterator =  const dummy_array_iterator_type<T, dummy_array<T, Size> const, Size>;

...so I really don't understand what is happening. Can someone please clarify?

TIA

Wad
  • 1,454
  • 1
  • 16
  • 33
  • 1
    I think you should create another type of iterator, or rename the original iterator to something like `const_dummy_array_iterator_type` and have another non-const version of it, just like in C++ standard library does (`const_iterator` and `iterator`) – tnt Feb 14 '19 at 13:47

2 Answers2

1

I found a way to enable T& operator*() only when C is not constant:

    template <class Tp = T>
    typename std::enable_if<std::is_const<C>::value, Tp>::type const& operator* () const 
    { 
        return collection.GetAt(index); 
    }

    template <class Tp = T>
    typename std::enable_if<!std::is_const<C>::value, Tp>::type & operator* () const 
    { 
        return collection.GetAt(index); 
    }

I have no idea about the syntax (which I get from https://stackoverflow.com/a/26678178)

tnt
  • 1,174
  • 2
  • 10
  • 14
  • 1
    Thanks; this is a good workaround. However, I am probably going to open a bounty on this because I need to understand why my code as-is does not work; please don't be offended! – Wad Feb 15 '19 at 11:59
1

dummy_array_const_iterator::operator * should always return T const & regardless of constness of the iterator object itself.

The easiest way to achieve this is probably just to declare it with T const as underlying iterator value type:

template <typename T, size_t const Size> 
using dummy_array_const_iterator = dummy_array_iterator_type<T const, dummy_array<T, Size> const, Size>;

Since you are returning the iterator by value its constness can be easily lost by c++ type deduction rules and just declaring dummy_array_const_iterator as alias to const dummy_array_iterator_type is not enough. i.e. the following fails:

#include <type_traits>

struct I { };
using C = I const;
C begin();

int bar()
{
    auto x = begin(); // type of x is deduced as I
    static_assert(std::is_same<I, decltype(x)>::value, "same"); // PASS
    static_assert(std::is_same<decltype(begin()), decltype(x)>::value, "same"); // ERROR
}
dewaffled
  • 2,850
  • 2
  • 17
  • 30
  • OK, I've tried that and it compiles. It even compiles if I change `=const dummy_array_iterator_type` to `= dummy_array_iterator_type'. But I still don't understand: the `const` form of `operator*()` *should* have been called on a `const dummy_array_iterator_type` but it wasn't: **why?** And how does this code now faciliate the `const` version of `dummy_array_iterator_type::operator*()` to be called, even when the `dummy_array_iterator_type` is NOT `const`? Can you please elaborate in your answer (where you have more space)? – Wad Feb 16 '19 at 16:14
  • @Wad I've added an example of constness loss to the answer. Hope this clarifies the observed behavior. – dewaffled Feb 16 '19 at 18:36
  • Thanks, I understand how `auto` drops the `const`. 1) I am thus assuming that the `range for` will use the `auto` keyword to deduce the types of the iterators, hence this problem arises; is this correct? 2) I thought that your fix would cause the 'const` form of 'operator*()' to be called, but having stepped though the code **it doesn't!**; it is the none-`const` form that is always called; I can remove the `const` form completely! I am now totally confused, can you possibly shed light on exactly what your fix facilitates? – Wad Feb 16 '19 at 20:55
  • 1
    1) yeah, this behavior specified in standard - https://en.cppreference.com/w/cpp/language/range-for#Explanation 2) If you want the same behavior as standard library iterators then you should leave only `const` form (and remove remove non-const) - it will be invoked for both const and non-const objects and if you do the opposite dereferencing of const objects will not compile. – dewaffled Feb 16 '19 at 21:42
  • Sorry, was thinking hard. Your fix, to change type *underlying value type* to `const`, works because in my code, for a constant iterator, the return type of `dummy_array_iterator_type::operator*()` (which is `T&`) becomes **const** `T&` i.e. **const** `int&`; it doesn't matter that the `dummy_array_iterator_type` itself is not `const`. Thus, an attempt to assign to an iterator of a `const dummy_array` will fail as we're trying to assign to `const int&`. When the `dummy_array` is not `const`, `operator*()` return an `int&` so we **can** assign to it! Please **confirm this is correct?** – Wad Feb 18 '19 at 11:44
  • Incidentally, as has been mentioned above, the STL has separate `const` and none-`const` iterators, so it'd probably be best if I do the same. But I just wanted to understand what was going on here! – Wad Feb 18 '19 at 11:44
  • Yes, this is exactly what happens. One more tricky thing with standard library non-const iterators - they return non-const reference from const dereference operators (by casting const away): https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/stl_list.h#L210 so underlying value can be modified even with const reference to non-const iterator. – dewaffled Feb 18 '19 at 15:13