右值引用和移动构造函数
以下程序都均在 VS2019测试
C++左值和右值
- C语言中区分左值和右值最为经典的判别方法是:在赋值表达式中,出现在等号左边的是“左值”,在等号右边的称为“右值”;
- C++ 还有一个更为广泛的说法:可以取地址的、有名字的就是“左值”,不能取地址的、没有名字的就是“右值”;
- 左值: 可以标识函数、对象的值。
- C++11中,右值由两个概念构成:将亡值和纯右值;
- 纯右值: 就是C++98标准中右值的概念,用于辨识临时变量和一些跟对象关联的值。如:临时变量、字面量、lambda表达式等。
- 将亡值: C++11 新增的词,和右值引用相关的表达式,这样表达式通常是要被移动的对象。比如:返回右值引用T&& 的函数返回值、std::move的返回值、转换为T&&的类型转换函数的返回值。
- C++11 程序中,所有值必属于左值、将亡值、纯右值三者之一。
深入理解C++11 --C++11新特性解析与应用
C++左值引用和右值引用
- 右值引用是C++11 引入,对一个右值进行引用的类型,用&&表示。
- C++98中的引用类型称为“左值引用”,声明时通过 &表示。一块内存的多个名称(别名),可以被程序的其他部分访问。
- 无论左值引用还是右值引用,都必须立即初始化。原因是:引用类型本身不具有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
- 右值引用不能绑定任何左值,即左值不能用于初始化右值;
- 只要能够绑定右值的引用类型,都能够延长右值的生命期。
#include <iostream>
int square(int a){ return a * a;}
int main()
{
int x = 12; // x是左值
std::cout << "左值 x : "<< x << ", 左值 x 地址: " << &x << std::endl;
int& x1 = x; //正确,x1是x的别名,x1和x指向同一块内存
std::cout << "左值 x1: " << x1 <<", 左值 x1 地址: "<< &x1 << std::endl;
x = square(5); // x是左值,square(5)是右值,更新内存中的值
std::cout << "左值 x : " << x << ", 左值 x 地址: " << &x << std::endl;
//int& x2 = square(5); // 不能编译,x2是左值,square(5)是右值
// int&& y = x; //右值初始化,错误,编译出错,左值不能用来初始化右值
int&& y = 20; //右值初始化,正确
std::cout << "右值 y : " << y << ", 右值 y 地址: " << &y << std::endl;
// int&& y1 = y; //不能编译,在初始化完成之后,y变成了一个左值
int&& y2 = square(5); //右值初始化,正确
std::cout << "右值 y2: " << y2 << ", 右值 y2 地址: " << &y2 << std::endl;
y2 = 10;
std::cout << "右值 y2: " << y2 << ", 右值 y2 地址: " << &y2 << std::endl;
const int& y3 = x * 42; //正确,我们可以将一个const的引用绑定到一个右值上
return 0;
}
结果:
左值 x : 12, 左值 x 地址: 000000D9B90FFBD0
左值 x1: 12, 左值 x1 地址: 000000D9B90FFBD0
左值 x : 25, 左值 x 地址: 000000D9B90FFBD0
右值 y : 20, 右值 y 地址: 000000D9B90FFBD4
右值 y2: 25, 右值 y2 地址: 000000D9B90FFBD8
右值 y2: 10, 右值 y2 地址: 000000D9B90FFBD8
- 常量左值引用是万能型的引用类型(从C++98开始),可以接受非常量左值、常量左值、右值对其初始化。使用右值对其初始化时,还可以像右值引用一样将右值的声明周期延长。不足点就是只能只读,不能修改其内容。
- 右值引用和移动语句紧密相关,右值存在的最大价值就是实现移动语句,另外一个价值就是用于转发。
左值、右值、类型判断
- 标准库在<type_traits>头文件提供了3个模板类来判断:
is_reference 、is_rvalue_reference 、is_lvalue_reference - 可配合操作符decltype对变量进行类型推导来得到引用类型
int main() {
std::cout << "std::string是否是引用: "<<std::is_reference<std::string>::value<<std::endl;
std::cout << "std::string&是否是引用: "<<std::is_reference<std::string&>::value<<std::endl;
std::cout << "std::string&&是否是引用: "<<std::is_reference<std::string&&>::value<<std::endl;
std::cout << "std::string&是否是右值引用: "<<std::is_rvalue_reference<std::string&>::value<<std::endl;
std::cout << "std::string&&是否是右值引用: "<<std::is_rvalue_reference<std::string&&>::value<<std::endl;
std::cout << "std::string&是否是左值引用: " << std::is_lvalue_reference<std::string&>::value << std::endl;
std::cout << "std::string&&是否是左值引用: "<<std::is_lvalue_reference<std::string&&>::value<<std::endl;
std::string szUT = "123";
std::cout << "szUT是否是引用: " << std::is_reference<decltype(szUT)>::value << std::endl;
return 0;
}
std::string是否是引用: 0
std::string&是否是引用: 1
std::string&&是否是引用: 1
std::string&是否是右值引用: 0
std::string&&是否是右值引用: 1
std::string&是否是左值引用: 1
std::string&&是否是左值引用: 0
szUT是否是引用: 0
std::move()函数
- C++11中, std::move()在头文件< utility >中
- std::move() 的唯一作用是将左值强制转换为右值引用,并不能移动任何东西。在实现上讲,std::move基本等同于一个类型转换:static_cast< T&& >(value)
- 真正的移动发生在移动构造函数和移动赋值运算符函数中
- std::move()的作用只是为了让调用构造函数时告诉编译器选择移动构造函数。
- 注意:使用std::move()输入的参数,在调用之后,不能在使用了,因为已经把所以权转移了,若是指针最好转移后赋值为nullptr
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
int x = 10;
std::cout << " x : " << x << ", x 地址: " << &x << std::endl;
int& z = x; //左值引用,z是x的别名,z和x的内存地址相同
std::cout << " z : " << z << ", z 地址: " << &z << std::endl;
// int&& y = x; //y是右值,x是左值,编译失败
int&& y = std::move(x); //y是右值,x是左值,y和x的内存地址相同
std::cout << " x : " << x << ", x 地址: " << &x << std::endl;
std::cout << " y : " << x << ", y 地址: " << &y << std::endl;
std::cout << " z : " << z << ", z 地址: " << &z << std::endl;
std::string szT = "Test";
std::vector<std::string> vecT;
vecT.push_back(szT);
std::cout << "szT: " << szT << std::endl;
vecT.push_back(std::move(szT));//调用move()函数后,最好不要使用szT,以为所有权已经转移出去了,内容为空
std::cout << "After move,szT: " << szT << std::endl;
for_each(vecT.begin(), vecT.end(), [](std::string szT) {std::cout << "Vector : "<< szT << std::endl; });
return 0;
}
结果:
x : 10, x 地址: 00000040E8CFFD08
z : 10, z 地址: 00000040E8CFFD08
x : 10, x 地址: 00000040E8CFFD08
y : 10, y 地址: 00000040E8CFFD08
z : 10, z 地址: 00000040E8CFFD08
szT: Test
After move,szT:
Vector : Test
Vector : Test
std::move()函数只是将传入的值转换为右值,此外没有其他动作。std::move()用到移动构造函数或者移动赋值运算符,能充分起到减少不必要拷贝的意义。
std::forward函数
- 完美转发:指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另一个函数。
- C++11 是通过引入一条所谓“引用折叠”的新语言规则,并结合新的模板推导规则来完成完美转发。
- std::move(): 无条件地把它的参数转换成右值,实际没有move任何东西
- std::forward< T >(u): 若u是左值,则传递之后仍然是左值;若u是右值,则传递之后仍然是右值。T为u的类型,输入右值时, std::forward< T >(u)相当于 std::move(),也会把所有权转移走
#include<iostream>
template<typename T>
void Print(T& t) { std::cout << "Left value." << std::endl;}
template<typename T>
void Print(T&& t) { std::cout << "Right value." << std::endl;}
template<typename T>
void Test(T&& t)
{
Print(t); //t无论时左值还是右值,都会转为左值(右值引用参数作为函数的形参,在函数内部再转发该参数的时候会变成一个左值)
Print(std::forward<T>(t)); //t为右值输入右值,t为左值输入左值
Print(std::move(t)); // t无论时左值还是右值,都会转为右值
}
int main()
{
Test(1); //1是右值
std::cout << "--------------------------------" << std::endl;
int x = 1;
Test(x); //x是左值
std::cout << "--------------------------------" << std::endl;
Test(std::forward<int>(x)); //x是左值,但x表示的是1,而1为右值,传入右值
return 0;
}
Left value.
Right value.
Right value.
--------------------------------
Left value.
Left value.
Right value.
--------------------------------
Left value.
Right value.
Right value.
代码copy自:c++11 完美转发 std::forward()
移动构造函数
- 在 C++ 11 标准之前(C++ 98/03 标准中),若想用一个对象初始化一个同类的新对象,只能使用拷贝构造函数;
- 创建和拷贝时需要对对象的每个成员进行初始化,若成员占内存比较大(指针成员指向非常大的堆内存数据),每次申请内存,消耗比较大。具体可参考 复制构造函数
- C++11标准后,可使用移动构造函数省去临时变量申请内存的开销。
#include <iostream>
class Demo
{
public:
Demo() { std::cout << "Constructor" << std::endl; }
Demo(const Demo&) { std::cout << "Copy Constructor" << std::endl; }
Demo(const Demo&&) { std::cout << "Move Constructor" << std::endl; }
virtual ~Demo() {std::cout << "Destructor Constructor" << std::endl;}
};
Demo GetDemo()
{
Demo objDemo; //调用构造函数创建对象objDemo
return objDemo; //调用移动构造函数把objDemo的内存所有权移给临时对象,然后调用析构函数析构objDemo,只是把objDemo析构了,里面已转移所有权的参数的内存中的内容还在(转移给了临时对象)
}
int main()
{
Demo objDemo = GetDemo(); //因为GetDemo()返回值是临时对象,属于右值,因此将调用移动构造函数,会把临时对象的资源转交给对象objDemo,若没有移动构造,会默认调用复制构造
return 0; //调用析构函数析构对象objDemo1
}
Constructor
Move Constructor
Destructor Constructor
Destructor Constructor
- 类内有移动构造函数时,会优先调用移动构造函数;若无移动构造会默认调用拷贝构造。因此,通常由移动构造函数时都会显示声明复制构造函数。
带有指针成员的类时移动构造函数
#include <iostream>
struct Test
{
int i = 0;
std::string szT = "123";
};
class Demo
{
public:
Demo() /*:m_pTest(new Test())*/
{
m_pTest = new Test();
std::cout << "Constructor,m_pTest address: "<< m_pTest << std::endl;
}
Demo(const Demo& obj) /*:m_pTest(new Test(*(obj.m_pTest)))*/
{
m_pTest = new Test(*(obj.m_pTest));
//m_pTest = new Test(std::move(*(obj.m_pTest))); //添加std::move()和不添加这是一样的效果
std::cout << "Copy Constructor,m_pTest address: " << m_pTest << std::endl;
}
Demo(Demo&& obj)/* :m_pTest(obj.m_pTest)*/
{
m_pTest = obj.m_pTest;
obj.m_pTest = nullptr;
std::cout << "Move Constructor,m_pTest address: " << m_pTest << std::endl;
}
virtual ~Demo()
{
std::cout << "Destroy Constructor,m_pTest address: " << m_pTest << std::endl;
if (nullptr != m_pTest)
{
std::cout << "Release Memory"<< std::endl;
delete m_pTest;
m_pTest = nullptr;
}
}
private:
Test* m_pTest = nullptr;
};
Demo GetDemo()
{
Demo obDemo; //调用构造函数,创建对象obDemo,obDemo中m_pTest的地址是A
std::cout << "================================================" << std::endl;
return obDemo; //调用移动构造函数把对象objDemo中的m_pTest的所有权转移给临时对象,然后调用析构函数析构objDemo,因为m_pTest是所有权转移了,所以析构时m_pTest的地址是空的,不需要释放内存,而临时对象的m_pTest的地址是A(若移动构造函数不存在,会默认调用复制构造函数)
}
int main()
{
Demo objDemo = GetDemo(); //因为GetDemo()返回值是临时对象,属于右值,因此将调用移动构造函数,会把临时对象的资源转交给对象objDemo,对象objDemo中m_pTest的地址是A,
std::cout << "================================================" << std::endl;
Demo objDemo1(objDemo); // 调用复制构造函数,用对象objDemo 初始化objDemo1,obDemo1中m_pTest的地址是B
std::cout << "================================================" << std::endl;
Demo objDemo2(std::move(objDemo)); //std::move语句将左值变为右值而避免拷贝构造,调用移动构造函数把对象objDemo中的m_pTest所有权转移给objDemo2,对象objDemo中m_pTest的地址是空,对象objDemo2中m_pTest的地址是A
std::cout << "================================================" << std::endl;
return 0;
//调用析构函数,析构objDemo2对象,因为objDemo的所有权转移给了objDemo2,所以objDemo2需要释放内存
//调用析构函数,析构objDemo1对象,因为objDemo1复制了一份objDemo,所以需要释放内存
//调用析构函数,析构objDemo对象,因为objDemo的所有权转移给了objDemo2,所以不需要释放内存
}
结果:
Constructor,m_pTest address: 0000021CB34BF900
================================================
Move Constructor,m_pTest address: 0000021CB34BF900
Destroy Constructor,m_pTest address: 0000000000000000
================================================
Copy Constructor,m_pTest address: 0000021CB34BF4B0
================================================
Move Constructor,m_pTest address: 0000021CB34BF900
================================================
Destroy Constructor,m_pTest address: 0000021CB34BF900
Release Memory
Destroy Constructor,m_pTest address: 0000021CB34BF4B0
Release Memory
Destroy Constructor,m_pTest address: 0000000000000000
拷贝构造需要对成员变量进行深拷贝,而移动构造不需要,所以移动构造效率更高。
移动构造函数使用示例
自定义字符输出流,重载<<,每个输入字符流自动添加[]包裹
#include <iostream>
#include <sstream>
class CCustomStream
{
public:
CCustomStream(): m_ssLog(new std::stringstream)
{
std::cout << "Constructor,m_ssLog address: " << m_ssLog << std::endl;
}
CCustomStream(const CCustomStream& obj)
{
m_ssLog = new std::stringstream(std::move(*(obj.m_ssLog)));
std::cout << "Copy Constructor,m_ssLog address: " << m_ssLog << std::endl;
}
CCustomStream(CCustomStream&& obj)
{
m_ssLog = obj.m_ssLog;
obj.m_ssLog = nullptr;
std::cout << "Move Constructor,m_ssLog address: " << m_ssLog << std::endl;
}
virtual ~CCustomStream()
{
std::cout << "Destroy Constructor,m_ssLog address: " << m_ssLog << std::endl;
if (nullptr != m_ssLog)
{
std::cout << "Release Memory" << std::endl;
delete m_ssLog;
m_ssLog = nullptr;
}
}
std::string str() const
{
return m_ssLog->str();
}
template<typename T>
CCustomStream& operator<<(T&& t)
{
std::cout << "operator" << std::endl;
*m_ssLog << "[" << t << "]";
return *this;
}
private:
std::stringstream* m_ssLog;
};
int main()
{
CCustomStream objCS; //调用构造函数,创建对象objCS
std::cout << "objCS: " << objCS.str() << std::endl;
objCS << "dkjn"<<"152"; //输入值
std::cout << "objCS: "<<objCS.str() << std::endl;
std::cout <<"================================================" << std::endl;
CCustomStream objCS1(objCS); //调用复制构造函数,初始化对象objCS1
std::cout << "objCS1: " << objCS1.str() << std::endl; //objCS1把objCS内存中的值也复制过来了
std::cout << "objCS: " << objCS.str() << std::endl; //objCS内存中的值没了
objCS1 << "dfh" << "188";
std::cout << "objCS1: " << objCS1.str() << std::endl;
std::cout << "objCS: " << objCS.str() << std::endl;
std::cout << "================================================" << std::endl;
CCustomStream objCS2(std::move(objCS)); //调用移动构造函数,初始化对象objCS2
std::cout << "objC2: " << objCS2.str() << std::endl; //objCS2和objCS使用同一块内存,但objCS2并没有要objCS的值
objCS2 << "sfsf" << "65151";
std::cout << "objC2: " << objCS2.str() << std::endl;
// std::cout << "objCS: "<< objCS.str() << std::endl; //因为objCS2是移动的objCS的所有权,所以objCS不还在,会发生异常
std::cout << "================================================" << std::endl;
return 0;
}
结果:
Constructor,m_ssLog address: 0000025FE49EAAE0
objCS:
objCS: [dkjn][152]
================================================
Copy Constructor,m_ssLog address: 0000025FE49E5690
objCS1: [dkjn][152]
objCS:
objCS1: [dkjn][152][dfh][188]
objCS:
================================================
Move Constructor,m_ssLog address: 0000025FE49EAAE0
objC2:
objC2: [sfsf][65151]
================================================
Destroy Constructor,m_ssLog address: 0000025FE49EAAE0
Release Memory
Destroy Constructor,m_ssLog address: 0000025FE49E5690
Release Memory
Destroy Constructor,m_ssLog address: 0000000000000000
参考资料
复制构造函数
理解std::move和std::forward
C++:浅谈右值引用
C++右值引用(std::move)
C++移动构造函数
C++深拷贝和浅拷贝(深复制和浅复制)完全攻略
C++的std::move与std::forward原理大白话总结
C++11 std::move和std::forward
c++11 特性之std-move的使用和原理
c++11 之右值引用、移动语义、std::move
C++11右值引用和std::move语句实例解析
深入理解C++11 --C++11新特性解析与应用