#include<iostream>
#include<stdio.h>
using namespace std;
class Test
{
public:
string n;
Test():n("test") {}
};
int main()
{
Test t1;
std::cout<<"before move"<<"\n";
std::cout<<"t1.n=" << t1.n<<"\n";
Test t2=std::move(t1);
std::cout<<"after move"<<"\n";
std::cout<<"t1.n="<<t1.n<<"\n";
std::cout<<"t2.n="<<t2.n<<"\n";
return 0;
}
上述程序的输出产生以下结果
移动前t1.n =测试 搬家后 t1.n = t2.n =测试
理解在将对象t1移动到t2之后,t2.n的值导致为空字符串
但是相同的概念移动概念不适用于整数。
#include<iostream>
#include<stdio.h>
using namespace std;
class Test
{
public:
int n;
Test():n(5) {}
};
int main()
{
Test t1;
std::cout<<"before move"<<"\n";
std::cout<<"t1.n=" << t1.n<<"\n";
Test t2=std::move(t1);
std::cout<<"after move"<<"\n";
std::cout<<"t1.n="<<t1.n<<"\n";
std::cout<<"t2.n="<<t2.n<<"\n";
return 0;
}
上述程序的输出产生以下结果
移动前t1.n = 5 搬家后 t1.n = 5 t2.n = 5
在将对象t1移动到t2之后,我预期t2.n的值为0但旧值仍然存在。
有人可以解释这种行为背后的概念。
答案 0 :(得分:3)
在C ++中移动是其他语言中所谓的浅拷贝。
复制:如果对象将数据存储在动态分配的内存中,则(深)复制意味着(1)分配等效的内存块,(2)将所有元素从一个块复制到其他。这将保留复制的对象并创建一个完全独立的新副本。
移动:如果移动了同一个对象,则只复制与对象实际存储的数据(而不是存储在动态分配的内存中的数据)(实际上,它们被移过,即这是递归的,即指针变量保存内存块的地址和有关大小的信息。同时,移动的对象被“清空”,即进入(有效)状态,该状态在破坏时不会影响移动的对象。这意味着
必须将指向移动对象的内存块的指针重置为nullptr
,并将指向内存大小的变量重置为零,您调用的进程清空。
现在,对于不使用动态分配内存的对象,特别是所有内置类型(例如int
),移动和复制之间没有区别。特别是,将移动的变量保持在其原始状态,即制作副本,就可以了,并且实际上是标准所要求的。 (标准可能没有指定或要求重置为默认值,即0,但事情并非如此。)
另请参阅here了解移动语义的详细说明。
答案 1 :(得分:2)
通常,移动对象可以具有对其类型有效的任何值。例如,移动的std::string
可能会变空,或者可能是完全不同的东西。它有时可能是空的,有时不是。它可以是什么没有限制,不应该依赖确切的值。
由于移动的对象可以处于任何有效状态,我们可以看到复制对象是移动它的有效方式。实际上,对于没有定义移动构造函数的任何类型,在移动对象时将使用复制构造函数。 std::move
不要求移动的对象变为空(我们不一定要为所有类型定义的概念)。当然,复制可能不是移动对象的最有效方法,但它是允许的。原始类型基本上都是基于此,因此移动基本类型相当于复制它。
我想再强调一下:不要(通常)依赖于移动对象的值。它通常不是指定的值。 不要假设移动的对象与默认构造的对象相同。 不要假设它是任何其他值。某些特定类型(如int
或标准智能指针类型)可以指定移动的值,但这些是特殊情况,并不定义一般规则。在将已知值复制到其中之前,通常不使用移动的对象。
答案 2 :(得分:1)
如果您通常需要将内置类型设置为0(例如int,float,pointers),因为您可能会在销毁时依赖它们,例如用户定义的API指针,您需要显式编写一个move运算符以将这些成员设置为零。
class MyClass
{
std::string str_member_; // auto moved and emptied
int a_value_; // need to manually set to 0
void* api_handle_; // ''
public:
// boilerplate code
MyClass(MyClass&& rhd)
{
*this = std::move(rhd);
}
MyClass& operator=(MyClass&& rhd)
{
if (this == &rhd)
return *this;
str_member_ = std::move(rhd.str_member_);
a_value_ = rhd.a_value_;
api_handle_ = rhd.api_handle_;
rhd.a_value_ = 0;
rhd.api_handle_ = 0;
return *this;
}
};
我通常不喜欢这样,因为当新成员添加到类中时,它容易出错。他们需要添加到样板代码中。相反,您可以使用小型帮助程序类,该类在使用默认移动语义进行移动时将特定成员设置为0。
template<class T>
class ZeroOnMove
{
T val_;
public:
ZeroOnMove() = default;
ZeroOnMove(ZeroOnMove&& val) = default;
ZeroOnMove(const T& val) : val_(val) {}
ZeroOnMove& operator=(ZeroOnMove&& rhd)
{
if (this == &rhd)
return *this;
val_ = rhd.val_;
rhd.val_ = 0;
return *this;
}
operator T()
{
return val_;
}
};
在此之前的课程仅是:
class MyClass
{
public:
std::string str_member_;
ZeroOnMove<int> a_value_;
ZeroOnMove<void*> api_handle_;
};
也许这也有助于更多地了解移动语义。
答案 3 :(得分:0)
隐式Move构造函数不适用于成员非类类型 但是,如果您使用显式的Move构造函数,则在这种情况下,可以将交换函数用于非类类型。
#include <utility>
std::exchange(old_object, default_value) //explicit move of a member of non-class type
下面是示例
#include<iostream>
#include<string>
#include <utility>
struct A
{
std::string name;
int age;
A(){
std::cout << "Default ctor. ";
}
//explicit
A(std::string const& s, int x):name(s), age(x){
std::cout << "Ctor. ";
}
A(A const& a):name(a.name),age(a.age){
std::cout << "Copy ctor. ";
}
A(A && a) noexcept :name(std::move(a.name)),age(std::exchange(a.age,0)){
std::cout << "Move ctor. ";
}
A& operator=(A const& a){
std::cout << "Copy assign. ";
name = a.name;
age = a.age;
return *this;
}
A& operator=(A && a) noexcept {
std::cout << "Move assign. ";
name = std::move(a.name);
age = std::move(a.age);
return *this;
}
void printInfo()
{
std::cout<<name<<" "<<age<<std::endl;
}
~A() noexcept {
std::cout << "Dtor. ";
}
};
int main()
{
A a("StackOverflow ", 12);
a.printInfo();
A b = std::move(a);
b.printInfo();
a.printInfo();
return 0;
}
了解更多信息 https://en.cppreference.com/w/cpp/language/move_constructor