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?
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?
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.
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.
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
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.