为什么 --string::end() 编译但 --string.size() 不编译?

Why does --string::end() compile but --string.size() does not?

代码

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

创建编译器错误 lvalue required as decrement operand 但是

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

没有。这是为什么?

string::size returns 标量类型(某种整数),按值和内置前缀 - -- 表达式不允许出现在纯右值上。相比之下,对于 overloaded 运算符(在用户定义的类型上),它们本质上只是函数,没有这样的限制。因此,是否允许 --str.end() 取决于 string::iterator 是用户定义类型(某种 class 类型)还是标量类型(例如指针)。不过,这并未指定,甚至可能因优化设置而异。

基本类型算术类型的递减(和递增)运算符的操作数必须是左值。 std::string::size returns 整数类型的值(即不是引用),因此函数调用 str.size() 是纯右值。纯右值不是左值,所以 --str.size() 是病式的。

class 类型的递减(和递增)运算符重载的操作数可以在右值上调用,除非重载是左值引用限定的。如果 --str.end() 是合式的,那么我们可以推断迭代器是 class 类型(指针是不是 class 类型的迭代器类型的一个例子)和递减运算符重载确实没有左值引用限定符。据我所知,标准容器迭代器的成员函数是否引用限定是未指定的。

P.S。这同样适用于(复合)赋值运算符:赋值的 l 左手操作数也必须是左值(除非它是具有非左值重载的 class 类型参考资格)。事实上,这就是 l 中的“l”值的来源。

我想补充一点,您 可以 自定义用户定义的成员函数,例如 ++,当通过左值和右值引用调用时,它们的行为会有所不同。例如,您可以定义一个自定义的 iterator class 来阻止您在临时对象上调用修改成员函数。为此,您需要使用 reference qualifiers.

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

使用这些限定符,您仍然允许修改左值:

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

但您现在可以防止修改临时对象,这会导致用户定义的 class 与 size_t.

等内置类型更加一致
++(iterator{}); // ERROR

Live example here

我不确定标准对 std::string 的迭代器类型是怎么说的,但原则上,您可以对自己的迭代器和其他 classes 执行此操作,无论您身在何处认为修改临时文件总是错误的。