C++ 移动具有整数成员的对象的语义
C++ Move semantics with object having integer member
#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,但旧值仍然存在。
谁能解释一下这种行为背后的概念。
移动在C++中就是其他语言中所谓的浅拷贝。
copy:如果对象将数据存储在动态分配的内存中,则(深)复制意味着 (1) 分配等效的内存块和 (2) 复制所有元素从一个街区到另一个街区。这会保留复制自对象并创建一个新的、完全独立的副本。
move:如果移动同一个对象,那么只复制与对象实际存储的数据(而不是存储在动态分配内存中的数据)(实际上,它们被移动,即这是递归的),即指针变量保存内存块的地址和有关大小的信息。同时,移出对象是 'emptied',即进入(有效)状态,销毁时不会影响移入对象。这意味着
指向移出对象的内存块的指针必须重置为 nullptr
,并且引用内存大小的变量重置为零,您调用的过程 emptying。
现在,对于不使用动态分配内存的对象,尤其是所有内置类型(例如int
),移动和复制之间没有区别。特别是,将移出的变量保留在其原始状态(即制作副本)就很好,并且实际上是标准所要求的。 (标准可能对此未指定或要求重置为默认值,即 0,但事实并非如此。)
另请参阅 here 以了解对移动语义的冗长描述。
通常,移出的对象可以具有对其类型有效的任何值。例如,一个 moved-from std::string
可能会变成空的,也可能是完全不同的东西。它有时可能是空的,有时不是。可以是什么都没有限制,不应该依赖确切的值。
由于移出对象可以处于任何有效状态,我们可以看到复制对象是移动对象的有效方式。事实上,对于任何没有定义移动构造函数的类型,移动对象时都会使用复制构造函数。 std::move
不要求移出的对象变为空(我们不一定为所有类型定义一个概念)。当然,复制可能不是移动对象的最有效方式,但它是允许的。原始类型基本上利用了这一点,因此移动原始类型等同于复制它。
我想再次强调这一点:不要(通常)依赖移出对象的值。它通常不是指定值。 不要假设移出的对象与默认构造的对象相同。 不要 假设它是任何其他值。某些特定类型,例如 int
或标准智能指针类型可能会指定移出值,但这些都是特殊情况,并未定义一般规则。通常明智的做法是在将已知值复制到其中之前不要使用移出的对象。
隐式 Move 构造函数对于非 class 类型的成员无法正常工作
但是如果你使用显式移动构造函数,那么你可以使用非class类型的交换函数。
#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("Whosebug ", 12);
a.printInfo();
A b = std::move(a);
b.printInfo();
a.printInfo();
return 0;
}
了解更多信息
https://en.cppreference.com/w/cpp/language/move_constructor
如果您通常需要将内置类型设置为 0,如 int、float、指针,因为您可能在销毁时依赖它们,例如用户定义的 API 指针,您需要显式编写移动运算符以将这些成员设置为零。
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;
}
};
我通常不喜欢这样做,因为在 class 中添加新成员时容易出错。他们需要添加到样板代码中。相反,您可以使用一个小助手 class,在使用默认移动语义移动时将特定成员设置为 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刚到:
class MyClass
{
public:
std::string str_member_;
ZeroOnMove<int> a_value_;
ZeroOnMove<void*> api_handle_;
};
也许这也有助于更多地理解移动语义。
#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,但旧值仍然存在。
谁能解释一下这种行为背后的概念。
移动在C++中就是其他语言中所谓的浅拷贝。
copy:如果对象将数据存储在动态分配的内存中,则(深)复制意味着 (1) 分配等效的内存块和 (2) 复制所有元素从一个街区到另一个街区。这会保留复制自对象并创建一个新的、完全独立的副本。
move:如果移动同一个对象,那么只复制与对象实际存储的数据(而不是存储在动态分配内存中的数据)(实际上,它们被移动,即这是递归的),即指针变量保存内存块的地址和有关大小的信息。同时,移出对象是 'emptied',即进入(有效)状态,销毁时不会影响移入对象。这意味着
指向移出对象的内存块的指针必须重置为 nullptr
,并且引用内存大小的变量重置为零,您调用的过程 emptying。
现在,对于不使用动态分配内存的对象,尤其是所有内置类型(例如int
),移动和复制之间没有区别。特别是,将移出的变量保留在其原始状态(即制作副本)就很好,并且实际上是标准所要求的。 (标准可能对此未指定或要求重置为默认值,即 0,但事实并非如此。)
另请参阅 here 以了解对移动语义的冗长描述。
通常,移出的对象可以具有对其类型有效的任何值。例如,一个 moved-from std::string
可能会变成空的,也可能是完全不同的东西。它有时可能是空的,有时不是。可以是什么都没有限制,不应该依赖确切的值。
由于移出对象可以处于任何有效状态,我们可以看到复制对象是移动对象的有效方式。事实上,对于任何没有定义移动构造函数的类型,移动对象时都会使用复制构造函数。 std::move
不要求移出的对象变为空(我们不一定为所有类型定义一个概念)。当然,复制可能不是移动对象的最有效方式,但它是允许的。原始类型基本上利用了这一点,因此移动原始类型等同于复制它。
我想再次强调这一点:不要(通常)依赖移出对象的值。它通常不是指定值。 不要假设移出的对象与默认构造的对象相同。 不要 假设它是任何其他值。某些特定类型,例如 int
或标准智能指针类型可能会指定移出值,但这些都是特殊情况,并未定义一般规则。通常明智的做法是在将已知值复制到其中之前不要使用移出的对象。
隐式 Move 构造函数对于非 class 类型的成员无法正常工作 但是如果你使用显式移动构造函数,那么你可以使用非class类型的交换函数。
#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("Whosebug ", 12);
a.printInfo();
A b = std::move(a);
b.printInfo();
a.printInfo();
return 0;
}
了解更多信息 https://en.cppreference.com/w/cpp/language/move_constructor
如果您通常需要将内置类型设置为 0,如 int、float、指针,因为您可能在销毁时依赖它们,例如用户定义的 API 指针,您需要显式编写移动运算符以将这些成员设置为零。
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;
}
};
我通常不喜欢这样做,因为在 class 中添加新成员时容易出错。他们需要添加到样板代码中。相反,您可以使用一个小助手 class,在使用默认移动语义移动时将特定成员设置为 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刚到:
class MyClass
{
public:
std::string str_member_;
ZeroOnMove<int> a_value_;
ZeroOnMove<void*> api_handle_;
};
也许这也有助于更多地理解移动语义。