C++中运算符重载的这些方法有何不同?

5
#include<iostream>
using namespace std;
class X
{
    int i;

    public:
    X(int a=0) : i(a) {}

    friend X operator+ (const X& left,const X&right);  

};
X operator+ (const X& left,const X&right)  // Method 1
{
    return X(left.i + right.i);
}

X operator+ (const X& left,const X&right) // Method 2
{
    X temp(left.i + right.i);
    return temp;
}

int main()
{
    X a(2),b(3),c;

    c=a+b;

    c.print();
    return 0;
}

在这段代码中,运算符+通过两种不同的方法进行了重载。
我的问题是:这些方法之间有什么区别,哪一个更实用?

1
在C++11中,return {left.i + right.i}是第三种方法,它也可以通过一些微不足道的优化实现相同的效果。 - Yakk - Adam Nevraumont
4个回答

6
我看不出任何编译器会在这两个版本之间生成不同的代码。第二个版本略微冗长,但是在这种情况下,编译器可以优化掉概念上的额外复制,我不知道有哪个编译器不会做到这种省略。
话虽如此,这只是微小的优化:编写最清晰的代码,这也带我来到我的最后一点。不要编写这两个运算符,而是与+=一起编写惯用的版本。
X& operator+=(const X&right) { i += right.i; return *this; }
X operator+(X left, const X& right) { return left += right; }

+1. @MichaelSmith:我认为你应该选择这个答案,它给出了关于如何设计你的操作符的正确建议。 - Andy Prowl
如果您计划将代码变成constexpr,请先编写operator +,然后再根据其编写operator += - CTMacUser

3
这两种方法没有区别,你应该选用最适合你意图的那一个。
关于“省略拷贝”的第12.8/31段规定:
当满足某些条件时,即使拷贝/移动操作符所选构造函数和/或对象的析构函数有副作用,实现也可以省略类对象的拷贝/移动构造。在这种情况下,实现将忽略被省略的拷贝/移动操作的源对象和目标对象,将其视为指向同一对象的两种不同方式,并在不进行优化的情况下销毁该对象的时间中较晚的时间进行该对象的销毁。这种省略拷贝/移动操作的称为“省略拷贝”,下列情况允许这种优化(可以组合使用以消除多个拷贝):
- 在带有类返回类型的函数的return语句中,当表达式是具有与函数返回类型相同的cv-unqualified类型的非易失自动对象的名称(不包括函数或catch子句参数)时,可以通过将自动对象直接构造到函数的返回值中来省略拷贝/移动操作; - [...]
如您所见,创建临时变量和命名具有自动存储期的局部对象的id-expression都符合省略拷贝的要求。
此外,编译器将把本地变量temp视为rvalue(在此情况下,视为临时变量),以便从函数中返回它。C++11标准的第12.8/32段规定:
当满足省略拷贝操作的条件或者源对象是函数参数,但要复制的对象由lvalue指定,选择复制构造函数的重载解析首先被视为如果对象是由rvalue指定的。[...]
因此,我强烈建议删除返回类型中的const限定符。
    const X operator + (const X& left, const X&right)
//  ^^^^^
//  Don't use this!

在C++11中,这将禁用移动语义,因为您无法从const对象移动,即使它是一个rvalue-简而言之,提供了移动构造函数的X也不会被选中,而是会调用复制构造函数。

第一个不是使用“RVO”概念吗?如果我错了,请纠正我。 - S J
@MichaelSmith:这两个表单将被完全相同地处理,并且在两种情况下都执行复制省略。让我添加标准的引用。 - Andy Prowl
@AndyProwl - 所以从编译器的角度来看,这些方法确实存在差异? - user2494601
@MichaelSmith:在C++03中,移动语义并不存在,这是有道理的。然而,在C++11中,应该避免使用这种习惯用法,因为(正如我在答案中所写)它使得从返回值中移动变得不可能,因为它是const - Andy Prowl
1
@MichaelSmith:好的,但不要习惯了 ;) 我的建议是要知道C++03中它的作用和存在意义(以防你需要处理旧代码),但编写新代码时应使用C++11。 - Andy Prowl
显示剩余9条评论

2
区别在于方法1使用了一个叫做“返回值优化”的概念。
方法1:
当编译器发现你创建的对象除了返回以外没有其他用途时,它会利用这一点,“直接在该值应该返回的位置构建对象”。在这里,只需要一个普通的构造函数调用(不需要复制构造函数),并且没有析构函数调用,因为你实际上从未创建本地对象。这更加高效。
方法2:
首先创建一个名为temp的临时对象。然后复制构造函数将temp复制到外部返回值的位置。最后,在作用域结束时调用temp的析构函数。
最终,方法1更加高效,但这是依赖于编译器的特性。

4
任何一个非落后的现代编译器都会为这两个版本生成完全相同的代码。另外,您忘记了移动语义。 - Griwes
但是在方法2中,临时对象被创建,并包括其构造函数调用。 - S J
@Griwes - 在RVO的情况下,编译器可以跳过复制构造函数的调用。我刚从维基页面上读到了这个信息。它是真的! - user2494601
@MichaelSmith,他并没有说他在谈论复制构造函数,完全误用了“临时”这个术语。至于can/can't - 我知道,但是在两种情况下都将执行完全相同的优化(假设我们正在谈论现代、非落后的编译器)。 - Griwes
他说在第一种情况下不需要调用复制构造函数!是的,方法1是编译器特性。在现代,两者被认为是相同的。 - user2494601
显示剩余2条评论

0
第二个实现将导致NRV优化。Stan Lippman说NRV优化需要一个显式的复制构造函数,但是这里的X类足够简单,所以我认为NRV不需要显式的复制构造函数。

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