【问题标题】:Why does operator ++ return a non-const value?为什么运算符 ++ 返回一个非常量值?
【发布时间】:2011-06-10 11:44:07
【问题描述】:

我已阅读 Scott Meyers 编写的 Effective C++ 3rd Edition。

本书的第 3 项,“尽可能使用const”,说如果我们想防止右值被意外分配给函数的返回值,返回类型应该是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

这有什么原因吗?

【问题讨论】:

  • 无论如何,您都不应该在比较表达式的任何一侧使用 ++。
  • 这得到了多少错误答案。
  • 它现在是一个过时的技巧,因为它阻止了移动构造。现代的做法是使用&amp; ref-qualifier 将operator= 定义为默认值。
  • @Tom 我不是在拖钓。在你之前的三个人已经接受了他们误解了这个问题。我是有建设性的,这就是我改变标题的原因。我不回答这个问题,因为我不知道答案。有正当理由吗?
  • 我在这里使用了双引号:“可能是因为这些东西是在迈耶的书之前写的,现在修复它已经太晚了!”野外可能有一些代码可以完全满足您想要防止的...

标签: c++ constants return-value


【解决方案1】:

我很确定这是因为它会对右值引用和任何类型的 decltype 造成严重破坏。尽管这些功能不在 C++03 中,但众所周知它们即将到来。

更重要的是,我不相信任何标准函数都会返回 const 右值,这可能是直到标准发布后才考虑的问题。此外,const rvalues 通常不被认为是 Right Thing To Do™。并非所有非 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);

是否应该仅仅因为有时我们可能调用一个没有意义的函数而不允许这样做?不,当然不。或者“交换”怎么样?

std::string func() { return "Hello World!"; }
std::string s;
func().swap(s);

如果func() 生成一个 const 表达式,这将是非法的 - 但它是完全有效的,而且确实,假设 std::string 的实现没有在默认构造函数中分配任何内存,既快速又清晰/可读。

您应该意识到 C++03 右值/左值规则坦率地说是没有意义的。实际上,它们只是部分烘焙,并且是在允许一些可能的权利的同时不允许一些公然错误的最低要求。 C++0x 右值规则更健全,也更完整

【讨论】:

  • const rvalue 不是重言式吗?我的意思是,根据定义,没有地址的右值不是 const 吗?
  • @davka:不,它们只是不能绑定到非常量引用。它们本身根本不是 const。
  • 写右值并不能回答问题。该问题询问为什么迭代器前置和后置运算符返回左值。关于这个问题的规则在 C++11 中没有改变,所以我质疑这些规则是否更健全、更完整(至少就手头的问题而言)。
  • @David Hammen:Rvalues 与这个问题有关,它询问返回 const 右值以返回非 const 右值。我的回答清楚地说明了为什么 const 右值是不可取的,以及 C++11 如何解决返回非 const 右值的问题并保持所有优势。
  • 我认为像您的示例一样使用后递增迭代器是不正确的,但您指出的原因是合理且有趣的。
【解决方案2】:

如果it 是非常量,我希望*(++it) 可以让我对它所代表的事物进行可变访问。

但是,取消引用 const 迭代器只会产生对其所代表事物的非可变访问。 [编辑: 不,this is wrong too。我现在真的放弃了!]

这是我能想到的唯一原因。

正如您正确指出的那样,以下内容是错误的,因为原语上的 ++ 会产生一个右值(不能在 LHS 上):

int* p = 0;
(p++)++;

所以这里的语言似乎确实有些不一致。

【讨论】:

  • 不是OP所要求的(顺便说一句,其他cmets在哪里解释这个?删除了?)。 for 循环甚至没有使用问题中提到的行为。
  • @Konrad:是的。 (是的,他们是。)我的最后一段解释了 OP 认为 const iterator 会做的事情和它实际会做的事情之间的区别......以及它实际做的事情的原因(这是他所要求的) .该循环绝对使用该行为:该问题专门讨论了op++,这在const 对象上(应该)是不可能的。难以用无法迭代的东西进行迭代。
  • @Tom 我认为你错了,但你可以通过解释你在答案中的意思来说服我反对我。另外,请注意更改的问题标题。这个问题与迭代器完全无关(这只是一个例子),它与operator ++ 的返回类型有关。你的评论也是错误的:it 不是 const。不过,++it 的返回值是。这里没问题。
  • @Konrad:在对 OP 的含义发生争议时更改了标题。我建议你把它留给 OP 来澄清,以防你错了。也就是说,我同意你的观点。我已经重写了我的答案。
  • @Konrad @Tom 我想我不会混淆const iteratorconst_iterator。后递增的原始指针不能递增,例如int* p; (p++)++;。但是 gcc 中的迭代器可以。
【解决方案3】:

编辑这并没有真正回答 cmets 中指出的问题。如果有用的话,我就把帖子留在这里……

我认为这几乎是一个语法统一的问题,以实现更好的可用界面。当提供此类成员函数而不区分名称并仅让重载解析机制确定正确的版本时,您可以防止(或至少尝试)程序员产生与const 相关的担忧。

我知道这可能看起来自相矛盾,尤其是在你的例子中。但是,如果您考虑大多数用例,那是有道理的。采用像std::equal 这样的 STL 算法。无论您的容器是否为常量,您始终可以编写类似bool e = std::equal(c.begin(), c.end(), c2.begin()) 的代码,而无需考虑正确的开始和结束版本。

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

嗯,这正是我现在想到的。不知道有没有说服力……

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

【讨论】:

  • 这真的无关紧要。 std::equal 将按值获取迭代器,完全否定该特定迭代器值可能具有的任何常量。
  • @DeadMG - 也许你错过了现场......?关键是,如果您处于对容器有引用的 const 的上下文中,并且您需要调用 std::equal,例如,您不必担心调用这样的 c.beginConstVersion () 函数,你可以简单地调用 c.begin() 并且编译器会做到这一点。
  • @Itcmelo:除了返回一个const_iterator,而不是const iterator。这个问题及其背后的语义与访问容器无关——它们是关于访问函数返回的 value 。无论它是引用还是 const/可变引用,这都是正确的。
猜你喜欢
  • 1970-01-01
  • 2013-02-27
  • 2020-07-10
  • 2011-03-07
  • 2020-04-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-28
相关资源
最近更新 更多