方法:
Object test(){
Object str("123");
return str;
}
然后,我有两种方法可以调用它:
代码1:
const Object &object=test();
代码2:
Object object=test();
哪一个更好?如果没有优化,会在代码2中发生两次复制构造函数调用吗?
其他有什么区别?
对于code2,我认为:
Object tmp=test();
Object object=tmp;
对于code1,我认为:
Object tmp=test();
Object &object=tmp;
但是tmp将是方法之后的解构函数。所以它必须添加const?
是没有任何问题的代码1吗?
答案 0 :(得分:4)
在C ++ 11中,std::string
有一个移动构造函数/移动赋值运算符,因此代码为:
string str = test();
将(最坏的情况下)有一个构造函数调用和一个移动赋值调用。
即使没有移动语义,这也可能(可能)通过NRVO(返回值优化)进行优化。
基本上不要害怕按价值回归。
编辑:只是为了让它100%清楚发生了什么:
#include <iostream>
#include <string>
class object
{
std::string s;
public:
object(const char* c)
: s(c)
{
std::cout << "Constructor\n";
}
~object()
{
std::cout << "Destructor\n";
}
object(const object& rhs)
: s(rhs.s)
{
std::cout << "Copy Constructor\n";
}
object& operator=(const object& rhs)
{
std::cout << "Copy Assignment\n";
s = rhs.s;
return *this;
}
object& operator=(object&& rhs)
{
std::cout << "Move Assignment\n";
s = std::move(rhs.s);
return *this;
}
object(object&& rhs)
: s(std::move(rhs.s))
{
std::cout << "Move Constructor\n";
}
};
object test()
{
object o("123");
return o;
}
int main()
{
object o = test();
//const object& o = test();
}
你可以看到每个都有1个构造函数调用和1个析构函数调用 - NRVO在这里(正如预期的那样)启动复制/移动。
答案 1 :(得分:4)
两个示例都有效 - 在1个const引用中引用一个临时对象,但此对象的生命周期会延长,直到引用超出范围(请参阅http://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/)。第二个例子显然是有效的,而且大多数现代编译器都会优化掉额外的复制(如果使用C + 11移动语义则更好),所以出于实际目的,示例是等效的(尽管在2中你可以修改它)。
答案 2 :(得分:4)
让我们分析一下你的功能:
Object test() { Object temp("123"); return temp; }
在这里,您要构建一个名为temp
的局部变量,并从函数中返回它。 test()
的返回类型为Object
,表示您按值返回。按值返回局部变量是一件好事,因为它允许使用称为返回值优化(RVO)的特殊优化技术。会发生的是,编译器将忽略该调用并直接将初始化器构造到调用者的地址中,而不是调用对复制或移动构造函数的调用。在这种情况下,因为temp
有一个名字(是一个左值),我们称之为N(amed)RVO。
假设发生了优化,尚未执行任何复制或移动。这就是你从main
调用函数的方法:
int main() { Object obj = test(); }
主要的第一行似乎特别值得关注,因为您认为临时将在完整表达式结束时被销毁。我认为这是一个令人担忧的原因,因为您认为obj
不会被分配给有效对象,并且使用对const
的引用初始化它是一种保持活着的方法。
你是对的两件事:
const
对其进行初始化将延长其使用寿命但临时将被销毁的事实并不令人担忧。因为初始化程序是一个右值,所以它的内容可以从移动。
Object obj = test(); // move is allowed here
在copy-elision中进行保理,编译器将忽略对复制或移动构造函数的调用。因此,obj
将被初始化为“好像”调用了复制或移动构造函数。因此,由于这些编译器优化,我们几乎没有理由担心多个副本。
但是如果我们接受你的其他例子呢?如果我们将obj
限定为:
Object const& obj = test();
test()
返回Object
类型的prvalue。这个prvalue通常会在包含它的完整表达式的末尾被破坏,但由于它被初始化为const
的引用,它的生命周期将扩展到引用的生命周期。
此示例与上一个示例有何区别?:
obj
如果您不熟悉移动语义,那么第一个要点是明显的,但不是第二个。因为obj
是const
的引用,所以它无法移动,编译器无法利用有用的优化。将对const
的引用分配给右值仅在一小部分情况下有用(如DaBrain指出的那样)。相反,最好是在有意义的时候运用值语义并创建值类型的对象。
此外,您甚至不需要函数test()
,您只需创建对象:
Object obj("123");
但如果您确实需要test()
,则可以利用类型扣除并使用auto
:
auto obj = test();
您的上一个示例涉及左值参考:
[..]但是
tmp
将在方法后被破坏。我们必须添加const
吗?Object &object = tmp;
在方法之后调用tmp
的析构函数不。考虑到我上面所说的内容,tmp
被初始化的临时值将被移入tmp
(或者它将被省略)。 tmp
本身在超出范围之前不会破坏。所以不,不需要使用const
。
但是如果你想通过其他变量来引用tmp
,那么引用是好的。否则,如果您知道之后不需要tmp
,则可以从中移除:
Object object = std::move(tmp);
答案 3 :(得分:1)
代码1是正确的。正如我所说,C ++标准保证临时的const引用是有效的。它的主要用途是具有参考的多态行为:
#include <iostream>
class Base { public: virtual void Do() const { std::cout << "Base"; } };
class Derived : public Base { public: virtual void Do() const { std::cout << "Derived"; } };
Derived Factory() { return Derived(); }
int main(int argc, char **argv)
{
const Base &ref = Factory();
ref.Do();
return 0;
}
这将返回“Derived”。一个着名的例子是Andrei Alexandrescu的ScopeGuard,但是使用C ++ 11它甚至更简单。