0

Update:

Thanks to everyone who submitted an answer.

In short, the answer is that the "iterators" that begin() and end() return must be copyable.

Artyer proposed a nice workaround: Make an iterator class that contains a reference (or, alternatively, a pointer) to the non-copyable object. Below is example code:

struct  Element  {};

struct  Container  {

  Element  element;

  struct  Iterator  {
    Container *  c;
    Iterator  ( Container * c )  :  c(c)  {}
    bool  operator !=  ( const Iterator & end )  const  { return  c != end.c; }
    void  operator ++  ()  {  c  =  nullptr;  }
    const Element &  operator *  ()  const  {  return  c->element;  }
  };

  Iterator  begin  ()  {  return  Iterator ( this    );  }
  Iterator  end    ()  {  return  Iterator ( nullptr );  }

};

#include  <stdio.h>
int  main  ()  {
  Container  c;
  printf ( "main  %p\n", & c .element );
  for  (  const Element & e  :  c  )  {  printf ( "loop  %p\n", & e );  }
  return  0;
}

Original Question:

The below C++ code will not compile (at least not with g++ version 9.3.0 on Ubuntu 20.04).

The error message is:
use of deleted function 'Iterator::Iterator(const Iterator&)'

Based on the error, am I correct in concluding that the "iterators" returned by begin() and end() must be copyable? Or is there some way to use a non-copyable iterator that is returned by reference?

struct  Iterator  {

  Iterator  ()  {}
  //  I want to prevent the copying of Iterators, so...
  Iterator  ( const Iterator & other )  =  delete;

  bool        operator !=  ( const Iterator & other )  {  return  false;   }
  Iterator &  operator ++  ()  {  return  * this;  }
  Iterator &  operator *   ()  {  return  * this;  }

};

struct  Container  {

  Iterator  iterator;

  Iterator &  begin()  {  return  iterator;  }
  Iterator &  end()    {  return  iterator;  }

};

int  main  ()  {
  Container  container;
  for  (  const Iterator & iterator  :  container  )  {}
  //  The above for loop causes the following compile time error:               
  //  error: use of deleted function 'Iterator::Iterator(const Iterator&)'      
  return  0;
}
mpb
  • 1,277
  • 15
  • 18
  • The range-based for works with an assumption that the container it operates on provides iterators that are both copy constructible and copy assignable. Your usage `for ( const Iterator & iterator : container ) {}` is also wrong - it would rely on `begin()` and `end()` both producing iterators that, when dereferenced, can give an `const Iterator &`. – Peter Aug 30 '20 at 02:54
  • 4
    A non-copyable type is not an Iterator, since the concept of Iterator requires the type to satisfy the concept of CopyConstructible. – eerorika Aug 30 '20 at 02:59
  • [How to implement an STL-style iterator and avoid common pitfalls?](https://stackoverflow.com/questions/8054273/how-to-implement-an-stl-style-iterator-and-avoid-common-pitfalls/8054856) – Mooing Duck Aug 30 '20 at 03:52

4 Answers4

3

Yes, iterators must be copyable. This is because range iteration is logically equivalent to the following code:

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

        range_declaration = *__begin;
        loop_statement

    }

This is the C++11 version. C++17, and later, are slightly different, but the fundamental reason is the same: __begin and __end are auto, and not auto &, or something like that. They are a non-reference type. The "begin_expr" and "end_expr", in so many words, are the begin and end expressions that end up calling your custom begin() and end().

Even if your begin() and end() returns references, they get assigned to a non-reference type and, as such, must be copyable.

Note that even if this was not the case, the shown implementation is not very useful, since both references will always be so the same object, and so the begin and the end expression will end up being the same object, and always compare equal (hopefully).

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • The example code in the question was not intended to be useful. It was intended to be simple. Cheers! – mpb Aug 30 '20 at 03:29
2

[stmt.ranged]/1

The range-based for statement

for ( init-statement(opt) for-range-declaration : for-range-initializer ) statement

is equivalent to

{
  init-statement(opt)

  auto &&range = for-range-initializer ;
  auto begin = begin-expr ;
  auto end = end-expr ;
  for ( ; begin != end; ++begin ) {
      for-range-declaration = * begin ;
      statement
  }
}

Note that in the statements

auto begin = begin-expr ;
auto end = end-expr ;

begin and end get copied. Both Container::begin() and Container::end() returns Iterator & and begin-expr and end-expr are lvalue-expressions, then Iterator must be CopyConstructible. If Container::begin() and Container::end() returns Iterator && and begin-expr and end-expr are xvalue-expressions, then Iterator need to be MoveConstructible. If Container::begin() and Container::end() returns Iterator and begin-expr and end-expr are prvalue-expressions, then Iterator don't need to be CopyConstructible or MoveConstructible because of mandatory copy elision (since C++17), but note that iterators are considered as abstraction of pointers, they're supposed to be copyable in general.

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
1

A non-copyable type is not an Iterator, since the concept of Iterator requires the type to satisfy the concept of CopyConstructible.

That said, (unless I've missed it), standard doesn't technically range-based for loop to actually use Iterators. While your example does attempt to copy the "iterators" and therefore cannot work, with small change it can be fixed:

struct  Container  {
    Iterator  begin()  {  return  {};  } // return by prvalue
    Iterator  end()    {  return  {};  }
};

This doesn't work prior to C++17 because there would be a move from temporary object. As such, another change would be needed which is to make the type movable.

In a C++ range-based for loop, can begin() return a reference to a non-copyable iterator?

No, but it can return a value to such non-iterator.

eerorika
  • 232,697
  • 12
  • 197
  • 326
1

You can always make a class that stores your reference whose copy constructor just copies the reference:

template<class T>
struct RangeForRef : private std::reference_wrapper<T> {

    using std::reference_wrapper<T>::reference_wrapper;

    bool operator!=(const RangeForRef& other) {
        return this->get() != other.get();
    }
    void operator++() {
        ++(this->get());
    }
    decltype(auto) operator*() {
        return *(this->get());
    }

};

struct  Container  {

  Iterator  iterator;

  RangeForRef<Iterator>  begin()  {  return iterator;  }
  RangeForRef<Iterator>  end()    {  return iterator;  }

};

Though I suspect your iterator class should just hold a reference to your non-copyable stuff in the first place, like:

struct Container {

    NonCopyable data;

    Iterator begin() {
        // Iterator has a `NonCopyable*` member
        // And copy assign / construct copies the pointer
        return { data };
    }
    Iterator end() {
        return { data };
    }

};
Artyer
  • 31,034
  • 3
  • 47
  • 75