3

我试图找到使增量和减量运算符的重新版本和后期版本可分别重载的基本原理。
在我看来,在我所见过的任何类型的类的这些运算符的每个实现中,它们都是相同的运算符(=做同样的事情),只是在调用它时有所不同。对我来说,C++ 的设计者应该有一个
运算符 似乎更合乎逻辑,编译器会根据需要在读取值之前或之后调用它(或者,更有可能的是,在前一个或下一个序列点,这我认为是等价的) ++

所以,问题是:有没有人有一个案例/类的例子,这些案例/类的实现可能不同?或者有人知道/猜测这种设计选择背后的理由吗?


对于那些更喜欢看代码而不是阅读问题文本的人,这里是摘要:

对于什么类型(代表您想要的任何内容的用户定义类),以下两行没有相同的副作用是T有意义的:

T v;

v++;
++v;

编辑
在下面引用@Simple的评论,我希望能澄清这个问题:

如果编译器可以自己进行复制并进行预增量,为什么后增量(重载)在语言中


编辑 2
由于许多人显然不清楚这个问题,这里有另一种解释:

考虑以下两行:

b = a++;
b = ++a;

如果它是一个运算符(为了论证,我将调用运算符 +a+),第一行将由编译器翻译成

b = a;
+a+;

第二个进入

+a+;
b = a;
4

8 回答 8

2

Pre increment 在语句的其余部分之前增加变量,例如

x = 2;
y = ++x;

y == 3;
x == 3;

而 post increment 在语句的其余部分之后进行增量,

x = 2;
y = x++;

y == 2;
x == 3;

预增量稍微快一些,所以它应该是首选。需要注意的是,当两个运算符都在一个语句中使用时,行为是未定义的,所以像

x = 5;
x = x++ + ++x;

会在不同的语言中给出不同的结果。

于 2013-11-05T15:56:21.720 回答
2

您将如何实现 post-increment 的通用版本?

我猜:T operator++(int) { T tmp(*this); ++*this; return tmp; }

如果我的类型不可复制或复制成本高怎么办?

好吧,我更喜欢:

Proxy operator++(int) { return Proxy(++*this, 1); }

然后有类似的东西:

bool operator==(Proxy const& left, T const& right) {
    return left.value - 1 == right.value;
}

如果编译器可以自己进行复制并进行预增量,为什么还要在语言中使用后增量(重载)?

因为您认为编译器可以进行复制的假设是错误的,即使它持有也可能成本太高。

于 2013-11-05T16:15:17.520 回答
1

这种区别在复杂类型的迭代器中变得很重要。表达方式

*it++

给我迭代器当前指向的对象,并递增迭代器。如果迭代器前进后数据通常不会保存在内存中,则返回前一个对象变得困难。有两种方法:

  1. 在后增量中保留一份副本
  2. 延迟后推进

前一种方法仍然必须返回行为类似于迭代器的东西(至少关于operator*and operator->,但不能是指针,因为它还必须保持对象副本的所有权,因此返回代理:

struct iterator {
    value_type value;

    struct proxy {
        value_type value;
        value_type &operator*() { return value; }
        value_type *operator->() { return &value; }
    };

    value_type &operator*() { return value; }
    value_type *operator->() { return &value; }
    iterator &operator++(); // actual increment code
    proxy operator++(int) { proxy ret = { value }; ++*this; return ret; }
};

如果创建副本也很昂贵并且应该避免,您还可以延迟增量:

struct iterator {
    value_type value;
    bool needs_increment;

    value_type &operator*() { if(needs_increment) ++*this; return value; }
    value_type *operator->() { if(needs_increment) ++*this; return &value; }
    iterator &operator++(); // actual increment code, resets needs_increment
    value_type *operator++(int) { needs_increment = true; return &value; }
};
于 2013-11-05T16:27:33.137 回答
0

看下面的例子

int i = 5;
int x = i++;
cout << i << " " << x;

这将打印

6 5

int i = 5;
int x = ++i;
cout << i << " " << x;

这将打印

6 6

那么我们能推断出什么?
在后缀中,i先将 的值赋给 x,然后再i递增
在前缀中,i先将 的值递增,然后再赋给 x

于 2013-11-05T15:59:59.143 回答
0

我认为这个问题与(子)表达式的评估顺序和应用副作用的时间有关。例如,在 C# 中,(子)表达式的求值顺序是确定性的,并且会立即应用副作用。例如考虑以下 C# 代码

int x = 0;
int y = x++ + ++x;

此代码在 C# 中定义了行为。所以你只能实现一个增量运算符,编译器会以适当的方式使用它。

C++ 没有这种可能性。(子)表达式的求值顺序是未指定的,并且不会立即应用副作用。

于 2013-11-05T16:17:17.610 回答
0

因为这些操作符对于内置类型有不同的语义。表达式的值在前后递增/递减之间有所不同,即使两者都更改了操作数。

int a = 1;
(a++) == 1;
a = 1;
(++a) == 2;

允许分别重载它们允许为返回值创建类似的语义。

于 2013-11-05T15:55:05.600 回答
0

也许在延迟实现的线程环境中的某些东西?对于++a,您希望它阻塞,直到它更新a,以便您获取更新的值,但对于a++,您只需发送信号并继续处理。

于 2013-11-06T11:02:10.643 回答
-1

他们是两个独立的运营商,因为他们做两件不同(尽管相关)的事情。

预递增/递减将递增/递减变量并返回值。

int i = 0;
int j = ++i; // j is now 1

后递增/递减将递增/递减变量并返回值。

int i = 0;
int j = i++; // j is now 0

通常,这些运算符的实现如下所示(对于某些类型T):

T& T::operator++() // prefix overload
{
    *this = *this + 1;
    return *this;
}

T T::operator++(int) // postfix overload
{
    T prev = *this;
    ++(*this); // call prefix overload
    return prev;
}

如您所见,前缀重载不需要类型的额外副本,而后缀版本则需要。

由于大部分评论都围绕着为什么会这样的问题:

简短的回答是:因为 C 标准是这样说的(并且 C++ 从 C 继承了它)。

更长的答案是:

++a并且a++只是调用特定函数的简写符号。 ++a(对于给定的类型T)映射到T& T::operator++()orT& operator++(T&)并且a++映射到T T::operator++(int)or T operator(T&, int)。与所有运算符一样,您(作为程序员)可以将它们定义为针对相应类型执行任何您想做的事情(注意:通常认为重载运算符来做一些奇怪的事情是不好的做法,但标准不会阻止您这样做)。一般来说,如果您定义一个类型(例如迭代器),您可以通过提供类似的接口(例如重载适当的运算符)使其与内置类型(例如指针)的行为相匹配。但是,你可以决定你想要operator++()执行二次公式并operator++(int)进行傅里叶变换。因为它们是 2 个独立的功能,所以这是允许的。operator++(int)如果允许编译器基于将根据 定义的前提进行推断operator++(),则它们将绑定在一起。

C++ 中的运算符只不过是函数调用的简写符号。虽然根据其他运算符来实现多个运算符是很常见的,但标准并不要求这样做,因此编译器无法做出这样的假设。如果标准要求它,那么就会有很多假设的行为需要跟踪。

此外,++aand的行为a++是 C 的继承。存在许多利用其中一种行为的代码,并且在 C++ 标准中更改它会破坏与 C 的兼容性(除非您还制作了C标准的变化)。由于有许多现有代码利用了这些运算符的行为,因此您可能会做出重大的重大更改。

虽然在前增量方面实现后增量是很常见的,但您确实应该将这两个函数视为不同的函数(与您对operator==vs operator!=operator<operator>等的想法非常相似。仅仅因为某些东西很常见并不意味着该标准将甚至应该将其作为一项要求。

于 2013-11-05T16:16:21.463 回答