引用过载与仅传值+std::move相比如何?

51

据看来,关于C++0x的rvalue的主要建议是在类中添加移动构造函数和移动赋值运算符,直到编译器自动实现它们。

但如果你使用VC10,等待不是一个明智的策略,因为自动生成可能要等到VC10 SP1,或者在最坏情况下,VC11。很可能,等待这一点将需要数年时间。

这就是我的问题所在。编写所有这些重复的代码并不好玩。而且看起来也不舒服。但是对于那些被认为速度慢的类来说,这是一个可承受的负担。但对于数百甚至数千个较小的类来说,情况并非如此。

::叹气:: C++0x本应让我写更少的代码,而不是更多!

然后我想了想。这个想法很多人都有,我猜测。

为什么不只按值传递所有东西呢?std::move + 复制省略不会使这几乎达到最佳状态吗?

示例1 - 典型的Pre-0x构造函数

OurClass::OurClass(const SomeClass& obj) : obj(obj) {}

SomeClass o;
OurClass(o);            // single copy
OurClass(std::move(o)); // single copy
OurClass(SomeClass());  // single copy

缺点:对于右值,会造成不必要的拷贝。

示例2 - 推荐使用 C++0x 吗?

OurClass::OurClass(const SomeClass& obj) : obj(obj) {}
OurClass::OurClass(SomeClass&& obj) : obj(std::move(obj)) {}

SomeClass o;
OurClass(o);            // single copy
OurClass(std::move(o)); // zero copies, one move
OurClass(SomeClass());  // zero copies, one move

优点:理论上来说是最快的。
缺点:需要编写大量代码!

示例3-按值传递+std::move

OurClass::OurClass(SomeClass obj) : obj(std::move(obj)) {}

SomeClass o;
OurClass(o);            // single copy, one move
OurClass(std::move(o)); // zero copies, two moves
OurClass(SomeClass());  // zero copies, one move

优点:无需额外的代码。
缺点:在情况1和2中是一次浪费的移动操作。如果SomeClass没有移动构造函数,性能将受到严重影响。


你认为呢?以上操作所产生的移动成本相对于减少代码的好处来说是否可以接受?


5
您并非第一个想到这个的人 ;- ) - fredoverflow
2
@dean 如果我知道参数有移动构造函数,我会在我的代码中使用按值传递然后移动的方式。如果我不知道,我会使用右值引用重载。 - Johannes Schaub - litb
1
@FredOverflow:我已经读过那篇文章了。我喜欢它,但是其中有些部分是错误的(“最坏的情况下,性能不会更差”),并且对我来说太理论化了——没有任何测量数据。无论如何,我的文章并不是要表达新观点,而是要问这个问题:你会选择哪一个?显然这取决于任务和个人,但我对回答很感兴趣。(顺便说一句,我现在想知道编译器是否可以消除#3中的额外赋值语句。) - dean
2
@dean,我认为说“最坏的情况下,性能不会更差”是没有错的。这完全正确。如果您以后必须复制,那么可以省略它并直接修改参数。我认为该语句并不适用于此复制到成员的情况,在某些情况下性能确实可能会稍微差一点。 - Johannes Schaub - litb
请注意,即使您是正确的,这个论点只适用于构造函数。当您尝试将此应用于任何其他内容(operator=)时,您最终会得到不必要的副本。 - Mooing Duck
显示剩余5条评论
1个回答

8

我对你的问题很感兴趣,因为这是我接触到的新话题,所以进行了一些研究。

首先,你的叹息。

:: 叹气 :: C++0x应该让我写更少的代码,而不是更多的代码!

它的作用是让您在代码上获得更好的控制。 它确实做到了。 我建议使用额外的构造函数:

OurClass::OurClass(SomeClass&& obj) : obj(std::move(obj)) {}

在复杂和重要的情况下,我个人更喜欢冗长,因为它可以使我和可能阅读我的代码的读者保持警觉。

例如,C语言风格的强制类型转换 (T*)pT 和 C++标准的 static_cast<T*>(pT) 更加冗长 - 但是这是一个很大的进步。

其次,我对你的示例3中的最后一个测试用例有些怀疑。我认为可能涉及到另一个移动构造函数,用于从右值创建传递的参数。因此,我在我的新VS2010中创建了一些快速项目,并得到了一些澄清。我将在此处发布代码以及结果。

源代码:

// test.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include <utility>
#include <iostream>

class SomeClass{
    mutable int *pVal;
public:
    int Val() const { return *pVal; };
    SomeClass(int val){
        pVal = new int(val);
        std::cout << "SomeClass constructor(pVal = 0x" << std::hex << pVal << std::dec << ")" << std::endl;
    }
    SomeClass(const SomeClass& r){
        pVal = new int(r.Val());
        std::cout << "SomeClass copy constructor(pVal = 0x" << std::hex << pVal << std::dec << ")" << std::endl;
    }   
    SomeClass(const SomeClass&& r){
        pVal = r.pVal;
        r.pVal = 0;
        std::cout << "SomeClass move constructor(pVal = 0x" << std::hex << pVal << std::dec << ")" << std::endl;
    }
    ~SomeClass(){
        if(pVal)
            delete pVal;
        std::cout << "SomeClass destructor(pVal = 0x" << std::hex << pVal << std::dec << ")" << std::endl;
    }
};

class OtherClass{
    SomeClass sc;
public:
    OtherClass(int val):sc(val){
    }

请注意本节内容:
#if 1
    OtherClass(SomeClass r):sc(std::move(r)){
    }
#else
    OtherClass(const SomeClass& r):sc(r){
    }   
    OtherClass(const SomeClass&& r):sc(std::move(r)){
    }
#endif

...

    int Val(){ return sc.Val(); }
    ~OtherClass(){
    }
};

#define ECHO(expr)  std::cout << std::endl << "line " << __LINE__ << ":\t" #expr ":" << std::endl; expr

int _tmain(int argc, _TCHAR* argv[])
{
    volatile int __dummy = 0;
    ECHO(SomeClass o(10));

    ECHO(OtherClass oo1(o));            
    __dummy += oo1.Val();
    ECHO(OtherClass oo2(std::move(o))); 
    __dummy += oo2.Val();
    ECHO(OtherClass oo3(SomeClass(20)));  
    __dummy += oo3.Val();

    ECHO(std::cout << __dummy << std::endl);
    ECHO(return 0);
}

正如您所指出的,有一个编译时开关可以让我测试这两种方法。 结果最好在文本比较模式下查看,在左侧您可以看到#if 1编译,意味着我们检查了提出的解决方法,在右侧-#if 0,意味着我们检查了c++0x中描述的“正常”方式!
我错误地怀疑编译器会做一些愚蠢的事情;它在第三个测试用例中保存了额外的移动构造函数。
但是说实话,我们必须考虑在提出的解决方法中调用另外两个析构函数,但是考虑到不应在被销毁的对象上执行任何操作,这肯定是一个小缺点。仍然很好知道。
无论如何,我仍然认为最好在包装类中编写另一个构造函数。这只是几行代码的问题,因为所有繁琐的工作都已经在SomeClass中完成了,而SomeClass必须具有移动构造函数。

对于明确的代码价值,你的努力和观点都值得赞赏。 - neuro
额外的移动构造函数并不是一个很大的问题。只要避免不必要的复制构造函数,情况就会相当不错。SomeClass(const SomeClass&& r) 不应该编译... - aschepler
__dummy 应该改为 _dummy,因为有两个下划线 >o< - ikh

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