我测试了以下代码:
#include <iostream>
using namespace std;
class foo{
public:
foo() {cout<<"foo()"<<endl;}
~foo() {cout<<"~foo()"<<endl;}
};
int main()
{
foo f;
move(f);
cout<<"statement \"move(f);\" done."<<endl;
return 0;
}
输出结果为:
foo()
statement "move(f);" done.
~foo()
但是,我预计:
foo()
~foo()
statement "move(f);" done.
根据功能移动的源代码:
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
返回的对象是一个正确的值,那么为什么不立即销毁呢?
-------------------------------------------------- ---------------
我想我只是混淆了rvalue和rvalue参考。
我修改了我的代码:
#include <iostream>
template<typename _Tp>
constexpr typename /**/std::remove_reference<_Tp>::type /* no && */
/**/ mymove /**/ (_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
using namespace std;
class foo{
public:
foo() {cout<<"foo() at "<<this<<endl;} /* use address to trace different objects */
~foo() {cout<<"~foo() at "<<this<<endl;} /* use address to trace different objects */
};
int main()
{
foo f;
mymove(f);
cout<<"statement \"mymove(f);\" done."<<endl;
return 0;
}
现在我得到了我一直期待的东西:
foo() at 0x22fefe
~foo() at 0x22feff
statement "mymove(f);" done.
~foo() at 0x22fefe
答案 0 :(得分:14)
从对象移动不会改变其生命周期,只会改变其当前值。从foo
返回时,您的对象main
将被销毁,这是在您的输出之后。
此外,std::move
不会从对象移动。它只返回一个rvalue引用,其引用是对象,使可能从对象移动。
答案 1 :(得分:5)
当对象超出范围时会被破坏。从物体移动不会改变这一点;根据移动构造函数或移动赋值操作符的作用,移动后对象的状态可能不同,但尚未被销毁,因此实际的规则是从对象移动必须将其置于可以销毁的状态。
除此之外,正如@ R.MartinhoFernandes所指出的,std::move
没有做任何事情。它是对象的移动构造函数或移动赋值运算符,它执行任何需要完成的操作,并且不会在对std::move
的调用中应用;当移动的对象用于构造新对象(移动构造函数)或分配给现有对象(移动赋值运算符)时应用它。像这样:
foo f;
foo f1(f); // applies foo's copy constructor
foo f2(std::move(f)); // applies foo's move constructor
foo f3, f4;
f3 = f; // applies foo's copy assignment operator
f4 = std::move(f1); // applies foo's move assignment operator
答案 2 :(得分:1)
因为在一般情况下,移动可能发生在另一个翻译单元中。在您的示例中,对象甚至没有移动,只标记为可移动。这意味着std::move
的调用者不知道对象是否被移动,他只知道有一个对象,并且它必须在作用域的末尾调用析构函数该对象的生命周期。 std::move
仅将对象标记为可移动,它不执行移动操作或创建可以进一步移动的移动副本或类似的任何内容。
考虑:
// translation unit 1
void f( std::vector< int >&& v )
{
if( v.size() > 8 ) {
// move it
}
else {
// copy it as it's just a few entries
}
}
// translation unit 2
void f( std::vector< int >&& );
std::vector< int > g();
int main()
{
// v is created here
std::vector< int > v = g();
// it is maybe moved here
f( std::move( v ) );
// here, v still exists as an object
// when main() ends, it will be destroyed
}
在上面的例子中,翻译单元2如何决定是否在std::move
之后调用析构函数?
答案 3 :(得分:1)
你对这个名字感到困惑 - std::move
实际上并没有移动任何东西。它只是将左值引用转换(强制转换)为右值引用,并用于让其他人移动。
std::move
有用的地方是你有一个带有左值或右值引用的重载函数。如果使用简单变量调用此类函数,则重载分辨率意味着您将使用左值引用调用该版本。您可以向std::move
添加显式调用,而不是调用右值参考版本。除了在采用右值参考的函数中外,不涉及任何移动。
现在它被调用 move的原因是你有两个构造函数的常见用法,其中一个构造函数采用左值引用(通常称为复制构造函数),另一个采用右值引用(通常称为移动构造函数)。在这种情况下,向std::move
添加显式调用意味着您调用移动构造函数而不是复制构造函数。
在更一般的情况下,通常的做法是使重载函数接受lvalue / rvalue引用,其中左值版本生成对象的副本,而rvalue版本移动对象(隐式修改源对象以接管任何内存)它使用)。
答案 4 :(得分:1)
std::move
不会改变对象的生命周期。粗略地说,它只是一个static_cast
,它将非常量左值转换为非常量右值引用。
这是有用的重载决议。实际上,一些函数通过const左值引用(例如复制构造函数)获取参数,而通过非const右值引用(例如移动构造函数)获取其他参数。如果传递的对象是临时对象,则编译器调用第二个重载。这个想法是,在调用函数之后,暂时不能再使用(并且将被销毁)。因此,第二次重载可以取得临时资源的所有权,而不是应对它们。
但是,编译器不会为非临时对象执行此操作(或者,对于左值更正确)。原因是传递的对象具有保留在范围内的名称,因此仍然可以使用(如代码所示)。因此,它的内部资源可能仍然是必需的,如果它们已被移动到另一个对象,那将是一个问题。不过,您可以使用std::move
指示编译器可以调用第二个重载。它将参数转换为右值引用,并通过重载决策调用第二个重载。
以下略有改动的代码说明了这一点。
#include <iostream>
using namespace std;
class foo{
public:
foo() { cout << "foo()" << endl; }
~foo() { cout << "~foo()" << endl; }
};
void g(const foo&) { cout << "lref" << endl; }
void g(foo&&) { cout << "rref" << endl; }
int main()
{
foo f;
g(f);
g(move(f));
// f is still in scope and can be referenced.
// For instance, we can call g(f) again.
// Imagine what would happen if f had been destroyed as the question's author
// originally though?
g(static_cast<foo&&>(f)); // This is equivalent to the previous line
cout<<"statement \"move(f);\" done."<<endl;
return 0;
}
输出
foo()
lref
rref
rref
statement "move(f);" done.
~foo()
更新(问题更改为使用mymove
后)
请注意,新代码并未完全给出您在开头所说的内容。实际上它会报告两次拨打~foo()
而不是一次。
从显示的地址中我们可以看到类型foo
的原始对象,即f
在最后被销毁。正如以前的原始代码一样。正如许多人所指出的,f
仅在其范围的末尾(函数体main
)被销毁。情况仍然如此。
在~foo()
之前报告的对statement "mymove(f);" done.
的额外调用会销毁另一个f
副本的对象。如果您将报告副本构造函数添加到foo
:
foo(const foo& orig) { cout << "copy foo from " << &orig << " to " << this << endl;}
然后你得到一个类似于:
的输出foo() at 0xa74203de
copy foo from 0xa74203de to 0xa74203df
~foo() at 0xa74203df
statement "move(f);" done.
~foo() at 0xa74203de
我们可以推断,调用mymove
会产生对复制构造函数的调用,以将f
复制到另一个foo
对象。然后,在执行到达显示statement "move(f);" done.
现在自然的问题是这个副本来自?好的,请注意mymove
的返回类型:
constexpr typename /**/std::remove_reference<_Tp>::type /* no && */`
在此示例中,为简化起见,在简化之后,归结为foo
。也就是说,mymove
按值返回foo
。因此,创建一个副本以创建临时对象。正如我之前所说的,在创建它的表达式完成后才会销毁临时文件(嗯,此规则有例外但它们不适用于此代码)。这解释了对~foo()
的额外调用。
答案 5 :(得分:0)
假设代码如下:
void bar(Foo& a) {
if (a.noLongerneeded())
lastUnneeded = std::move(a);
}
在这种情况下,bar
的调用者无法知道函数在某些情况下可能最终调用传递的对象的析构函数。它会对该对象负责,并确保在以后任何时候调用它的析构函数。
因此规则是move
可以将有效对象转换为另一个但仍然有效的对象。仍然有效意味着调用该对象的方法或析构函数仍应产生定义良好的结果。严格来说,move
除了告诉接收者这样一个引用,它可以改变对象,如果这样做有意义的话,它本身什么都不做。所以它是收件人,例如移动构造函数或移动赋值运算符,它实际移动。这些操作通常要么根本不更改对象,要么设置一些指向nullptr
的指针,或者设置一些长度为零或类似的指针。但是,它们永远不会调用析构函数,因为该任务留给了对象的所有者。