为什么运算符++返回一个非常量值?

24

我已经阅读了Scott Meyers写的《Effective C++第三版》。

该书的第3条目,“尽可能使用const”,指出如果我们希望防止rvalue被意外地分配给函数的返回值,返回类型应该是const

例如,针对iterator的递增函数:

const iterator iterator::operator++(int) {
    ...
}
然后,一些意外被预防了。
iterator it; 

// error in the following, same as primitive pointer
// I wanted to compare iterators
if (it++ = iterator()) {
  ...
}

然而,在GCC中,诸如std::vector::iterator之类的迭代器并不返回const值。

vector<int> v;
v.begin()++ = v.begin();   // pass compiler check

这是否有一些原因呢?


4
无论如何都不应该在比较表达式的任意一侧使用++。 - Daniel Daranas
2
这得到了多少错误答案,真是令人惊讶。 - Konrad Rudolph
3
现在这种做法已经过时了,因为它会阻止移动构造。现代的做法是使用 & 引用限定符将 operator= 定义为默认。 - Johannes Schaub - litb
10
@Tom 我不是在恶作剧。 在你之前已经有三个人接受他们误解了问题。我是在做出建设性的贡献,这就是我更改标题的原因。我没有回答问题,是因为我不知道答案。这是一个有效的理由,对吗? - Konrad Rudolph
3
“我在这里使用双引号:‘可能是因为这些内容是在梅耶的书之前写的,而现在已经太晚去修正了!’很可能有一些代码已经在运行中,会做出你想要避免的事情…” - Nim
显示剩余8条评论
3个回答

12
我相信这是因为它会对rvalue引用以及任何形式的decltype造成混乱。即使这些特性不在C++03中,但已经被人们所知道。
更重要的是,我不认为任何标准函数返回const rvalues,这可能是在标准发布之后才被考虑到的问题。此外,const rvalues通常被认为不是正确的操作。并非所有非const成员函数的使用都是无效的,返回const rvalues会导致它们全部失效。
例如,
auto it = ++vec.begin();

这是完全有效的,而且确实是有效的语义,尽管不完全理想。考虑一下我的类,它提供了方法链。

class ILikeMethodChains {
public:
    int i;
    ILikeMethodChains& SetSomeInt(int param) {
        i = param;
        return *this;
    }
};
ILikeMethodChains func() { ... }
ILikeMethodChains var = func().SetSomeInt(1);

只因为有时候我们可能会调用一个没有意义的函数,难道就应该禁止这样做吗?当然不是。还有“swaptimization”呢?

std::string func() { return "Hello World!"; }
std::string s;
func().swap(s);
如果func()产生一个const表达式,这将是非法的,但实际上它是合法的。假设std::string的实现不会在默认构造函数中分配任何内存,那么它既快速又易读。你应该意识到的是,C++03的右值引用/左值引用规则并不合理。它们有效地只是部分烘焙,最少要求排除一些明显的错误,同时允许一些可能的正确性。C++0x的右值引用规则更加合理和完整。

“const rvalue” 不是一种自相矛盾的说法吗?我的意思是,rvalue 本身就没有地址,按定义来说不是已经是 const 了吗? - davka
2
@davka:不,它们只是无法绑定到非const引用。它们本身并没有以任何方式成为const。 - Puppy
谈论rvalues并不能回答这个问题。这个问题问的是为什么迭代器的前置和后置自增运算符返回lvalues。关于这个问题的规则在C++11中没有改变,所以我怀疑规则是否更加合理和完整(至少对于手头的问题而言)。 - David Hammen
1
@David Hammen:Rvalues与问题有着千丝万缕的联系,它询问了将const rvalues返回与将non-const rvalues返回之间的区别。我的回答清楚地展示了为什么const rvalues是不可取的,以及C++11如何解决返回non-const rvalues的问题并保持所有优势。 - Puppy
我认为像你的示例一样使用后置迭代器是不正确的,但你提出的原因是合理和有趣的。 - skymountain

2
如果 it 不是 const 类型,我希望 *(++it) 可以让我对它所表示的东西进行可变访问。
然而,对 const 迭代器进行解引用只能得到对它所表示的东西的非可变访问。[编辑:不,这也是错误的。this is wrong too。我现在真的放弃了!]
这是我所能想到的唯一原因。
正如你所指出的,以下代码是非法的,因为对基本类型进行 ++ 会产生一个右值(不能在左侧使用):
int* p = 0;
(p++)++;

所以,这里的语言似乎存在一些不一致。

7
这也不是楼主所问的(顺便问一下,其他解释的评论在哪里?已经被删除了吗?)。这个“for”循环甚至没有使用到问题中提到的行为。 - Konrad Rudolph
@Konrad:是的,没错。(而且,它们确实是这样。)我的最后一段解释了OP认为“const iterator”会做什么以及它实际上会做什么之间的区别……以及它实际上会做什么的原因(这也是他所问的)。循环绝对使用了这种行为:问题明确讨论了“op++”,这在“const”对象上应该是不可能的。很难用不能用于迭代的东西进行迭代。 - Lightness Races in Orbit
@Tom 我认为你错了,但是你可以通过解释你在答案中的意思来说服我相反。另外,请注意已更改的问题标题。该问题与迭代器完全无关(那只是一个例子),它是关于 operator ++ 的返回类型的。而且你的评论也是错误的:it 不是 const。然而,++it 的返回值是 const 的。这里没有问题。 - Konrad Rudolph
@Konrad:在关于原帖意思的争议中,是你改变了标题。我建议你让原帖作者澄清,以防你的理解有误。话虽如此,我认同你的观点。我已经重写了我的回答。 - Lightness Races in Orbit
1
@Konrad @Tom 我认为我没有混淆 const iteratorconst_iterator。例如,后置递增的原始指针无法递增,比如 int* p; (p++)++;。但是在gcc中,迭代器可以这样做。 - skymountain
显示剩余4条评论

-1

编辑正如评论中指出的那样,这并没有真正回答问题。但我还是会把帖子留在这里,以防它在任何情况下都有用...

我认为这基本上是一种语法统一的问题,以实现更好的可用接口。当提供这样的成员函数时,不区分名称,只让重载解析机制确定正确的版本,可以防止(或至少尝试防止)程序员对const相关的担忧。

我知道这可能看起来矛盾,特别是考虑到你的例子。但如果你考虑大多数用例,它是有意义的。以像std::equal这样的STL算法为例。无论您的容器是否为常量,您总是可以编写类似于bool e = std::equal(c.begin(), c.end(), c2.begin())的代码,而无需考虑begin和end的正确版本。

这是STL的一般方法。记住operator[]...牢记容器将与算法一起使用,这是合理的。尽管在某些情况下,您仍然可能需要定义具有匹配版本(iteratorconst_iterator)的迭代器。

嗯,这只是我现在想到的。我不确定它有多具有说服力...

附注:正确使用常量迭代器的方法是通过 const_iterator typedef。


2
这真的不相关。std::equal将按值获取迭代器,完全抵消该特定迭代器值可能具有的任何const属性。 - Puppy
@DeadMG - 也许你错过了这个点……?关键是,如果你在一个上下文中有一个指向容器的const引用,并且需要调用std::equal等函数,你不必担心调用c.beginConstVersion()函数,只需调用c.begin()即可,编译器会自动处理。 - Leandro T. C. Melo
@Itcmelo:除了返回const_iterator而不是const iterator之外,这个问题和它背后的语义与访问容器无关 - 它们与访问函数返回的有关。无论它是引用还是const / mutable引用,这都是正确的。 - Puppy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接