9

I'm creating a convenient display() function template for container types. The output for the last element is different from the rest, thus I check when myIterator != --cont.cend();. This works for std::vector, but won't work for std::array. Why?

Here's a MWE (not my actual code):

std::vector<double> vec({1,2});
std::array<double, 2> arr({{1,2}});
auto vecIt = --vec.end(); // OK
auto arrIt = --arr.end(); // error: lvalue required as decrement operand
Bathsheba
  • 231,907
  • 34
  • 361
  • 483
Jersey
  • 423
  • 4
  • 13

4 Answers4

16

Since this is , [expr.pre.increment] and [expr.post.increment] both have the restriction that:

The operand shall be a modifiable lvalue.

Now, neither vec.end() nor arr.end() are lvalues, but both of their types are implementation-defined (for array and for vector). In both cases, a simple pointer would satisfy all the iterator requirements for those containers - and this would be a type that uses builtin prefix- and postfix-increment. In that case, --c.end() would be ill-formed due to the restriction cited. Howver, if the iterator type is a class type, the restriction above doesn't apply - since we're not using the builtin increment operators - and invoking operator--() on a class does not have this restriction on it (though it could, if the member function were lvalue-reference-qualified).

So --c.end() for either vector or array isn't guaranteed to work, since if end() returns a pointer, this is ill-formed, and end() is allowed to return a pointer. On your particular implementation, vector's iterator has class type but array's iterator is just a pointer type, which is why the former works but the latter doesn't.

Prefer std::prev(c.end()), which will work for both container types for all implementations.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • More basically, the iterator requirements do not require `--` to work on any iterator rvalue. – T.C. Jan 11 '18 at 07:01
7

Never decrement an rvalue, even if it happens to compile. It's unintuitive for readers of the code.

Use std::prev instead.

auto it = std::prev(arr.end());
Sebastian Redl
  • 69,373
  • 8
  • 123
  • 157
4

It depends on how the iterator is defined.

It seems that for the class template std::array the iterator is defined as a pointer. So the functions begin, end. cbegin, cend return just the pointer. Thus as the pointer is returned by value you may not decrease it because an lvalue is required..

For the class template std::vector the iterator is defined as a user-defined class for which the operator --() is defined.

Consider the following demonstrative program

#include <iostream>

class Int
{
public:
    Int( int x = 0 ) : x ( x ) 
    {

    }

    Int & operator --()
    {
        --x;
        return *this;
    }

    friend std::ostream & operator <<( std::ostream &os, const Int &i )
    {
        return os << i.x;
    }
private:
    int x;
};

int f( int x )
{
    return x;
}

Int f( Int x )
{
    return x;
}

int main() 
{
    std::cout << --f( Int( 10 ) ) << std::endl;
//  error: lvalue required as decrement operand
//  std::cout << --f( 10 ) << std::endl;

    return 0;
}

Take into account that you can write

auto arrIt = std::prev( arr.end() );

instead of

auto arrIt = --arr.end();

provided that you include header <iterator>.

You can use the operator with the reverse iterators of the class template std::array because the Standard defines them explicitly like

typedef std::reverse_iterator<iterator> reverse_iterator;
typedef std::reverse_iterator<const_iterator> const_reverse_iterator;

and the user-defined class std::reverse_iterator defines the operator --() .

Here is a demonstrative program

#include <iostream>
#include <array>

int main() 
{
    std::array<double, 2> arr = { { 1, 2 } };

    auto it = --arr.rend();

    std::cout << *it << std::endl;

    return 0;
}
Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
3

It's a fact of life from the C++ standard.

--T.end() is required to work if T is a std::vector, a std::list, or a std::deque. This was correct up to and including C++11; there have been relaxations subsequent to that.

Bathsheba
  • 231,907
  • 34
  • 361
  • 483
  • 2
    Huh, do you have a citation for that? – Sebastian Redl Jan 10 '18 at 13:11
  • There's something here: https://stackoverflow.com/questions/5322104/how-portable-is-end-iterator-decrement – Bathsheba Jan 10 '18 at 13:12
  • Interestingly, that table 68 which lists `vector/list/deque` and having that optional ability has been changed in C++14 (table 101 in `23.2.3 Sequence containers`) to *include* `array/basic_string` as well. It would be interesting to see if OP gets the same problem with a C++14 or better compiler. – paxdiablo Jan 10 '18 at 13:15
  • 3
    It looks like the most recent draft, N4713, has resolved DR355, removing the accidental implication that `--c.end()` needs to compile. So this answer is no longer correct for more recent C++ versions. You can decrement the end iterator still (for bi-directional sequences), but not necessarily if it's an rvalue. – Sebastian Redl Jan 10 '18 at 13:23
  • @SebastianRedl: Looks like I've stumbled on something more complex than I had originally thought. (I had it in my head from the when I learnt C++ formally - way back in C++03.) I've wiki'd if folk want to make contributions to the answer. – Bathsheba Jan 10 '18 at 13:25
  • @SebastianRedl LWG DR355 was resolved [in April 2004](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1629.html), almost 14 years ago. Somebody is a little late to the party... – T.C. Jan 11 '18 at 06:57