Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't I decrement std::array::end()?

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
like image 579
Jersey Avatar asked Jan 10 '18 12:01

Jersey


3 Answers

Since this is language-lawyer, [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.

like image 165
Barry Avatar answered Oct 20 '22 23:10

Barry


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());
like image 27
Sebastian Redl Avatar answered Oct 20 '22 23:10

Sebastian Redl


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;
}
like image 3
Vlad from Moscow Avatar answered Oct 20 '22 23:10

Vlad from Moscow