2

The code

std::string str{ "foo" };
auto lastCharIndex{ --str.size() };

creates a compiler error lvalue required as decrement operand but

auto lastCharIterator{ --str.end() };

does not. Why is this?

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
swaltek
  • 143
  • 5
  • 4
    Because `string::end()` is a `string.iterator()` and has a defined `operator --()`. – user207421 Apr 24 '19 at 01:32
  • 1
    Possible duplicate of [Why can't I decrement std::array::end()?](https://stackoverflow.com/questions/48187786/why-cant-i-decrement-stdarrayend) – ShadowRanger Apr 24 '19 at 01:38
  • Also relevant: [how portable is end iterator decrement?](https://stackoverflow.com/q/5322104/364696) – ShadowRanger Apr 24 '19 at 01:39
  • because they are two different function. they do different thing. (yes, this is for `string`, but generically functions can return anything, they may simply return a const object (with overloaded `operator--`) and you still cannot decrements it) – apple apple Apr 24 '19 at 02:15

3 Answers3

4

string::size returns a scalar type (some kind of integer), by value, and the built-in prefix--- expression is not allowed on prvalues. By contrast, for overloaded operators (on user-defined types), which are essentially just functions, no such restriction applies. Whether --str.end() is allowed therefore depends on whether string::iterator is a user-defined type (some sort of class type) or a scalar type (e.g. a pointer). This is not specified, though, and may even vary depending on optimization settings.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
1

The operand of the decrement (and increment) operator for fundamental types arithmetic types must be an lvalue. std::string::size returns a value (i.e. not a reference) of an integer type, so the function invocation str.size() is a prvalue. Prvalues are not lvalues, so --str.size() is ill-formed.

The operand of the decrement (and increment) operator overload for a class type may be called on an rvalue unless the overload is lvalue ref-qualified. If --str.end() is well-formed, then we can deduce that the iterator is a class type (pointer would be an example of a iterator type that is not a class type) and the decrement operator overload indeed does not have a lvalue ref-qualifier. As far as I can tell, whether member functions of iterators of standard containers are ref-qualified is unspecified.

P.S. Same applies to (compound) assignment operators: The left hand operand of an assignment must also be an lvalue (unless it is a class type with an overload that is not lvalue ref-qualified). In fact, this is where the "l" in lvalue originates.

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

I'd like to add that you can customize user-defined member functions like ++ that behave differently when called through lvalue and rvalue references. For example, you could define a custom iterator class that prevents you from calling modifying member functions on a temporary. To do this, you need to use reference qualifiers.

class iterator {
public:
    iterator operator++() & { // for lvalues
        std::cout << "incrementing...\n";
    }
    iterator operator++() && = delete; // for rvalues
};

With these qualifiers, you still allow lvalues to be modified:

iterator it = ...;
++it; // totally fine

but you can now prevent temporaries from being modified, which results in a user-defined class that is more consistent with built-in types like the size_t.

++(iterator{}); // ERROR

Live example here

I'm not sure what the standard says about this for std::string's iterator type, but in principle, you could do this for your own iterators and other classes, wherever you think it's always an mistake to modify a temporary.

alter_igel
  • 6,899
  • 3
  • 21
  • 40