C++编译器如何扩展前缀和后缀operator++()运算符?

4

请考虑:

class Example
{
private:
    int m_i;

public:
    Example(int i) : m_i{i} {}

    //Post-fix
    Example operator++(int) {m_i++; return *this;}

    //Pre-fix
    Example& operator++() {m_i++; return *this;}

    void print() const {std::cout << m_i << '\n'}
};

我正在通过这个实验来确定编译器如何扩展对前缀和后缀运算符的调用。

例如,当我写下像这样的代码:

Example e = 1;
e++;

我期望它扩展到类似于“e.operator++(int)”这样的内容,或者更进一步,我期望

e++(2);

我希望将其扩展为类似于“e.operator++(2)”这样的内容,但实际上编译器却抱怨一些“no match for call to '(Example) (int)'”。

接下来,我好奇“++e”是如何神奇地转换为返回引用的“e.operator++()”方法的。

继续尝试,最终得到:

Example e = 1;
++e++;
e.print();

打印出了2,然后:

Example e = 1;
(++e)++;
e.print();

打印3。

我理解(++e)返回对对象的引用,然后将其后置递增一次,所以这是有道理的。我还怀疑“++e++”在这里给出了后缀运算符的优先级(正如我在另一个帖子中所读到的),因此这会递增后缀运算符返回的临时变量。这也是有道理的。这使我想到了像下面这样的表达式:

++++e
++e++++
++++e++
++++e++++

这些代码都被扩展了(它们都可以编译并得到预期的结果)。

那么,实际上内部发生了什么,编译器如何知道调用哪个operator++(),这些表达式如何扩展(特别是在前缀情况下)?"operator++(int)"中占位符变量的目的是什么?


2
占位符仅用于在重载解析期间使两个运算符不同。参数未指定,必须保持未使用状态。 - François Andrieux
1
阅读上面的链接,你在末尾举出的所有人为例子都缺乏序列点。 - Cory Kramer
1
为了使您的后缀运算符在语义上表现如预期,将其更改为:Example operator++(int) { return Example(m_i++); } - Christopher Oicles
重新打开;这里没有未定义的行为,我认为通用的“运算符重载”线程不是对这个问题的好回答。 - M.M
2
@CoryKramer 每个函数的入口和出口都有一个序列点(使用C++11之前的术语);而重载运算符就是函数。 - M.M
1个回答

4

“operator++(int)”中的占位变量的目的是什么?

因为 ++ 操作符有两个不同的功能:后缀-++ 和 前缀-++。因此,在重载它时必须有两个不同的函数签名。

编译器如何知道调用哪个 operator++()?

当您的代码使用前缀 ++(例如:++e;)时,将调用带有签名 operator++() 的函数。 当您的代码使用后缀 ++(例如:e++;)时,将调用带有签名 operator++(int) 的函数,并且编译器会提供一个未指定的虚假参数值。

技术上,operator++(int) 的实现可以使用虚假参数值。 并且您可以通过编写 e.operator++(5); 而不是 e++; 来传递您自己的值。但是这被认为是不好的编码风格 - 在重载运算符时,建议保留内置运算符的语义,以避免混淆阅读代码的人。

请注意,您当前的后缀 ++ 的实现不遵守此规则:正常语义是应该返回先前的值; 但是您的代码返回了更新后的值。

++e++++;

要解析此语句,您需要了解以下解析规则:

  • 标记是通过“最大的匹配”来解析的,即这意味着 ++ e ++ ++;(而不是一些一元 + 运算符)。
  • 语言语法从这些标记中确定哪些表达式是哪些运算符的操作数。此过程可以在优先级表中总结。

查看该表格可得出:++(((e++)++))。使用我之前提到的展开方法,这可以用函数调用符号表示:

((e.operator++(0)).operator++(0)).operator++();

在这种情况下,需要从左到右调用这些函数,因为在进行成员函数调用之前必须评估它所调用的表达式。因此,假设在此语句之前有Example e(1);,则按以下顺序发生函数调用:
- e.operator++(int) - 将e.m_i设置为2并返回一个临时值(我将其称为temp1作为伪代码),其中temp1.m_i2。 - temp1.operator++(int) - 将temp1.m_i设置为3,并返回temp2,其m.i3 - temp2.operator++() - 将temp2.m_i设置为4并返回temp2的引用。
注意:我的答案仅讨论重载运算符是成员函数的情况。也可以将++(两种形式)作为非成员来重载。在这种情况下,行为与我的描述不变,但“以函数调用符号书写”的表达式将采用不同的语法。

那么对于类似于“++++e++”这样的扩展怎么办?我会假设我的后缀优先级假设是正确的,这意味着编译器看到的是一些(相当粗略的)等价于(++(++(e++)))的东西。这正确吗?将其扩展为实际的函数调用,它会是e.operator++(int).operator++().operator()++吗? - AldenB

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