4

This code does not compile with range-v3 0.10.0 (or with master). It does compile with range-v3 0.9.1.

#include "range/v3/all.hpp"

struct double_it {
  using value_type = double;
  double x;
  double& operator*() { return x; }
  const double& operator*() const { return x; }
};
static_assert(ranges::readable<double_it>);

Which version is right ? In master, a type I is readable only if same_as<iter_reference_t<I const>, iter_reference_t<I>>. I don't understand why the return type of operator* should be the same as the one of operator* const.

Note: issue submitted on github here.

Bérenger
  • 2,678
  • 2
  • 21
  • 42

2 Answers2

3

Take a look at P1878, which has all the rationale for this late design change. Iterators represent indirections. In C++, const-ness is shallow, meaning it doesn't follow indirections. Whether you dereference an int* or an int*const, the result you get back is the same: int&. The top-level const doesn't -- and shouldn't -- matter. Iterators do as the pointers do. They must, because a pointer is a valid model of the iterator concept.

To make this distinction more explicit, in C++20, the readable concept is called indirectly_readable.

TL;DR: Don't make the reference type of your iterator depend on the const-ness of the iterator itself.

Eric Niebler
  • 5,927
  • 2
  • 29
  • 43
  • Which means `double& operator*() const { return x; }`? It makes sense actually. Thanks! – Bérenger Apr 03 '20 at 22:16
  • What is the process in which the Ranges TS working draft (N4560) is updated? I usually refer to that document when working with ranges, but I realize (from my answer and yours) that I should not rely solely on it. Or, in other terms, where should I turn if I want to (try to) stay up to date with range-v3 in terms of the underlying draft standardese? – dfrib Apr 04 '20 at 11:21
  • The C++20 draft standard. The Ranges TS is years it of date now and will not be updated. – Eric Niebler Apr 04 '20 at 15:37
  • But `double& operator*() const {return x;}` is wrong for a different reason. The reference returned by an iterator's `operator*` should be valid even after the lifetime of the iterator ends. The implication is that iterators should never return a reference to something stored in the iterator itself. These are so-called "stashing" iterators, and they cause STL algorithms all sorts of trouble. In this case, I would just return the `double` by value. If it's a large object, store it in the range and have the iterator return a reference to _that_. HTH. – Eric Niebler Apr 07 '20 at 05:16
1

This seems to be at the very least an inconsistency in range-v3 as compared to the Ranges TS working draft.

There's a quite sparse description as for why these same_as predicates were added to to the readable concept for iterators in range-v3 issue 1449.

Get the range library compiling with msvc 19.24.28319 with /std:c++17

...

- CPP_concept_fragment(readable_, (I),
+ CPP_concept_fragment(readable_,
+     requires (/*I const i*/) //
+     (
+         // { *i } -> same_as<iter_reference_t<I>>;
+         // { iter_move(i) } -> same_as<iter_rvalue_reference_t<I>>;
+         0
+     ) &&
+     same_as<iter_reference_t<I const>, iter_reference_t<I>> &&
+     same_as<iter_rvalue_reference_t<I const>, iter_rvalue_reference_t<I>> &&
      common_reference_with<iter_reference_t<I> &&, iter_value_t<I> &> &&
      common_reference_with<iter_reference_t<I> &&,
                            iter_rvalue_reference_t<I> &&> &&
      common_reference_with<iter_rvalue_reference_t<I> &&, iter_value_t<I> const &>
  );

It seems the same_as predicates in the concept's implementation are intended to implement the requirement of:

// { *i } -> same_as<iter_reference_t<I>>;
// { iter_move(i) } -> same_as<iter_rvalue_reference_t<I>>;

Which were present (as comments in range/v3/iterator/concepts.hpp) even prior to this implementation change.

However, afaics, none of these requirements are present in the working draft of [iterators.readable] of Ranges TS (nor in the current HEAD of ericniebler/stl2 from which the previous linked draft is generated).

[iterators.readable] Concept Readable

The Readable concept is satisfied by types that are readable by applying operator* including pointers, smart pointers, and iterators.

template <class In>
concept bool Readable =
  requires {
    typename value_type_t<In>;
    typename reference_t<In>;
    typename rvalue_reference_t<In>;
  } &&
  CommonReference<reference_t<In>&&, value_type_t<In>&> &&
  CommonReference<reference_t<In>&&, rvalue_reference_t<In>&&> &&
  CommonReference<rvalue_reference_t<In>&&, const value_type_t<In>&>;

It may be a good idea to report this as an issue to, at the very least, sort out why the range-v3 implementation seemingly differs from [iterators.readable] of Ranges TS.

dfrib
  • 70,367
  • 12
  • 127
  • 192