个人对移动语义一些理解
需要知道的一些概念
数据类型
(英语:Data type),又称资料型态、资料型别,是用来约束数据的解释。在编程语言中,常见的数据类型包括原始类型(如:[整数]、[浮点数]或[字符])。
值类别
这是语义层面概念。C++中,左值和右值是表达式的分类,一个表达式必然是左值或右值之一。C++11把上述左值性(lvalueness)扩充为更复杂的值类别(value category),包含左值(lvalue),纯右值(prvalue)和临终值(xvalue)三个基本分类(fundamental)
关于值类别,有一点需要注意,值类别是用于告诉编译器如何处理这个资源。
- 左值:意味着以后可能还会用这个资源。
- 右值:告诉编译器以后不会再用到这个资源,编译器做一些优化比如节省拷贝构造或者直接销毁等
右值和左值是一种看待资源的观点,没有绝对之分,不能单纯的用字面常量或者临时变量等定义去理解。一个变量是左值还是右值取决于表达式,根据表达式定义来推断某个变量是左值还是右值,以便于编译器处理变量代表的资源,即告诉编译器当前如何对待某些资源。在日常使用中,判断变量的值类别即是左值还是右值需要根据当前使用情况判断,举个例子
class String
{
public:
String(const char * other){ cout << "create\n"; }
~String() { printf("destory\n"); }
private:
char* m_string;
uint32_t m_size;
};
class Entity
{
public:
Entity(String& name):m_name(name){}
private:
String m_name;
};
int main()
{
// String tmp("mj")
Entity name("mj");
// ~tmp
}
main
函数 创建对象name
的时候,会创建一个临时变量(发生了隐式转换) 假设String tmp("mj")
,作为name
构造函数的参数,tmp
在main
函数中是右值(临时变量,只用一次),但是在name
构造函数中,tmp
(引用) 是个左值,因为可能后续也要用。
右值引用:例如 String&&
。右值引用本质上是引用了一个变量(和引用的定义有关)。有句话说,右值引用本身是个左值。在这里,需要理解:以一个类的构造函数为例
Entity(String&& name)
:m_name(name)
int main()
{
Entity name("mj");
}
- 右值引用是数据类型:对一个变量(资源)的引用,这个变量(资源)在其他函数视为右值;在本例中就是
main
函数中会创建一个String
的临时变量,这个变量在mian
函数视作右值(只用一次,临时的,编译器可以随意优化),但是传入 ‘Entity’ 的构造函数之后,是一个左值,因为可能还要用。 - 左值是类别,这个变量(资源)的引用在当前函数视为左值
右值引用属于右值的范畴,这里着重讲解右值引用,是 因为在实际引用中,参数更多通过引用的形式传递。
std::move
人为的进行值类别转换,转换成右值。一般来说让一个左值变成右值。这里变化只是语义变换,更改类值类别,不会修改数据,不是什么很神奇的东西,就相当于换个角度对待(一个不恰当的例子:番茄,有些人叫西红柿,本质是一个东西)。编译器可以根据值类别来确定是否进行优化。
为什么某些情况需要使用,因为有时候人比编译器知道的信息更多,知道某些资源只用一次,便可转化为右值。
具体应用
思考以下,为什么需要进行值类别转换?先给出结论:减少开销。
class String
{
public:
String(const char * other)
{
cout << "create\n";
m_size = strlen(other);
m_data = new char[m_size];
memcpy(m_data, other, m_size);
}
String(const String& other)
{
cout << "copy\n";
m_size = other.m_size;
m_data = new char[m_size];
memcpy(m_data, other.m_data, m_size);
}
void Print()
{
for (uint32_t i = 0; i < m_size; i++)
{
printf("%c", m_data[i]);
}
printf("\n");
}
~String()
{
delete m_data;
printf("destory\n");
}
private:
char* m_data;
uint32_t m_size;
};
class Entity
{
public:
Entity(String& name)
:m_name(std::move(name)){ }
void PrintName(){ m_name.Print(); }
private:
String m_name;
};
int main()
{
Entity name("mj");
name.PrintName();
}
首先来观察这段代码,一个很常见的代码。String
是一个类,用于存放字符串。Entity
中有一个成员变量就是 String
类的对象。思考一个问题,当创建一个 Entity 对象的时候,一种经常会出现的初始化便是给一个字符串常量作为参数。思考一下,会发生什么,会创建多少个 String 的对象。
运行结果:
create
copy
destory
mj
destory
观察运行结果:
create 代表以字符串常量为参数创建了一个 String 对象,copy 代表用拷贝构造方法创建了一个对象。即创建了两个 String 对象。而且在 调用 name.PrintName
之前,有一个 String 对象进行了析构。
可以知道,在创建 name 对象的时候,程序创建了一个临时的 String 对象,方便讨论记为 tmp 。为什么会创建一个 tmp,因为Entity的构造函数的参数是 String 类型,故发生了隐式转换,申明一个 tmp 对象,它的参数是 name 的参数 即 “mj”,然后,tmp作为 name 的构造函数参数。待 name 对象创建完成,tmp销毁。
为什么会发生copy,显而易见, Entity 有个成员变量时 String 类型,初始化的时候参数是 String 类型,故发生拷贝构造。
思考:
1. 在 main 函数中,一个临时的 String tmp 是左值还是右值。
2. 在 Entity的构造函数中,作为参数传入的 tmp,是左值还是右值。
答案:1. 右值 2.左值
右值告诉编译器,我只用一次。编译器会自动进行优化,本例中就是用完销毁,减少内存占用。
减小开销,优化性能
在这种情况下,如何优化能。仔细观察发现,tmp 是一个必须要创建的临时变量(右值),Entity 的成员变量 m_name 也要进行拷贝构造初始化。既然 tmp 本来只用一次然后销毁,那可不可以在 m_name 初始化的时候,把 tmp 的资源 “偷” 过来,这样实际上只需要 申请 一个 String 对象所需要的资源。减少了开销。
如何实现,需要给 String 类增添一个以 String右值引用(String&&)为参数的构造函数。这个右值引用是为了和左值引用( String&)区分开来,实现函数重载。
添加代码:
String(String&& other) noexcept
{
cout << "move\n";
m_size = other.m_size;
m_data = other.m_data; //不申请内存,直接指向 other 已分配的资源
other.m_size = 0;
other.m_data = nullptr;// 让 other 析构的时候,不释放资源,因为有其他对象使用
}
修改代码:
Entity(String&& name) //不要添加 const ,因为这个参数是会在构造 m_name 时候被修改的,添加了就相当于左值的效果了
:m_name(std::move(name))//为啥这里加std::move 因为在这个函数中,name 是一个左值,不修改的话 在构造 m_name 的时候编译器会调用 左值为参数的 构造函数 String(const String& other) 而不是 String(String&& other) noexcept
{ }
再次运行
create
move
destory
mj
destory
拓展使用 移动赋值运算符
添加赋值运算符重载
String& operator=(String&& other)noexcept
{
if (this != &other)
{
cout << "move\n";
delete[] m_data;
m_size = other.m_size;
m_data = other.m_data;
other.m_size = 0;
other.m_data = nullptr;
}
return *this;
}
执行代码
int main()
{
String firstName("jian");
String secondName("ma");
cout << "firstName:";
firstName.Print();
cout << "secondName:";
secondName.Print();
cout << "----------------\n";
firstName = std::move(secondName);//就好像把secondName的资源移动到 firstName 中。
cout << "firstName:";
firstName.Print();
cout << "secondName:";
secondName.Print();
}
运行结果:
create
create
firstName:jian
secondName:ma
----------------
move
firstName:ma
secondName:
destory
destory
总结:
移动语义可以帮助我们进行性能优化,减少拷贝。