1.左值和右值
左值是可以出现在等号左边的符号,当然它也可以出现在等号右边,例如int a等等。右值是能且只能出现在等号右边的符号,例如5,“abc”等等。右值细分为将亡值和纯右值,目前来看不必理会。判断一个值是否为左值有一个很粗暴的办法:是否能取地址。
2.左值引用和右值引用
对左值的引用即为左值引用,对右值的引用即为右值引用,例如下面的示例。左值只能对应左值引用,右值只能对应右值引用。
int a = 3;
int& left_ref = a;
int&& right_ref = 3;
//int& left_ref = 3; 报错
//int&& right_ref = a; 报错
3.移动拷贝构造函数和移动拷贝赋值函数
看下面代码即可,可以想到参数为MyString& 和MyString&&是对函数的重载。所谓移动拷贝构造函数和移动拷贝赋值函数,只是用右引用来做参数。
#include<vector>
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
class MyString {
private:
char* data;
size_t len;
void init_data(const char *s) {
this->len = strlen (s);
data = new char[this->len+1];
memcpy(this->data, s, this->len);
data[this->len] = '\0';
}
public:
MyString() {
this->data = NULL;
this->len = 0;
}
MyString(const char* p) {
this->init_data(p);
}
MyString(const MyString& str) {
std::cout << "Copy Constructor ! source: " << str.data << std::endl;
this->len = str.len;
init_data(str.data);
}
MyString(MyString&& str) {
std::cout << "Move Constructor ! source: " << str.data << std::endl;
this->len = str.len;
this->data = str.data;
str.len = 0;
str.data = NULL;
}
MyString& operator=(const MyString& str) {
std::cout << "Copy Assignment ! source: " << str.data << std::endl;
if (this != &str) {
this->len = str.len;
init_data(str.data);
}
return *this;
}
MyString& operator=(const MyString&& str) {
std::cout << "Move Assignment ! source: " << str.data << std::endl;
if (this != &str) {
this->len = str.len;
init_data(str.data);
}
return *this;
}
virtual ~MyString() {
if (this->data) free(data);
}
};
int main() {
MyString a;
a = MyString("Hello");
MyString b(a);//MyStrig b = a;也是拷贝构造函数的
b = a;
std::vector<MyString> vec;
vec.push_back(MyString("World"));
return 0;
}
#输出
Move Assignment ! source: Hello
Copy Constructor ! source: Hello
Copy Assignment ! source: Hello
Move Constructor ! source: World
这里还做了个小实验,如果把移动拷贝构造函数和移动拷贝赋值函数去掉,会怎么样,下面是输出。
Copy Assignment ! source: Hello
Copy Constructor ! source: Hello
Copy Assignment ! source: Hello
Copy Constructor ! source: World
看来是当作左值处理了。那么问题来了,为什么左值引用也能用右值来初始化呢?原因很简单,因为我的代码中用了常量左值引用,常量左值引用可以用右值来初始化的。如果去掉函数参数中的const修饰,那么会报错。
4.std::move()
有了上面的基础,我们就可以引出std::move()了。这个函数做的事情其实很简单,就是把传入的符号强行转换成右值引用。它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);
可见其函数流程是通过右值引用传递模板实现,利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。然后通过static_cast<>进行强制类型转换返回T&&右值引用,而static_cast<T>之所以能使用类型转换,是通过remove_refrence<T>::type模板移除T&&,T&的引用,获取具体类型T。
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
到此,我们就可以分析一下下面代码的流程了。main函数中第3行,就是先把符号a转化为右值,所以push_back()会调用string类的移动拷贝构造函数,参照MyString类型的移动拷贝构造函数,这样就节省了char*的深复制过程。
int main(){
vector<MyString> st;
MyString a = "123";
st.push_back(std::move(a));
return 0;
}
5.结合unique_ptr理解
我们再通过智能指针中的unique_ptr来深入了解一下其机制。unique_ptr的特点是独占式的,即它不允许执行拷贝赋值函数。如果需要传递所指向的地址,要使用std::move()函数。那unique_ptr是怎么做到这2个特点的呢,只需要看unique_ptr的实现即可。阅读下面代码,可以发现拷贝构造函数、拷贝赋值函数都被设置为delete,unique_ptr自然不可通过“=”传递值。unique_ptr实现了移动拷贝构造函数和移动拷贝赋值函数,自然也就能通过pb = std::move(pa)这样的语法来传递值了。
template<typename T>
class MyUniquePtr
{
public:
explicit MyUniquePtr(T* ptr = nullptr)
:mPtr(ptr)
{}
~MyUniquePtr()
{
if(mPtr)
delete mPtr;
}
MyUniquePtr(MyUniquePtr &&p) noexcept;
MyUniquePtr& operator=(MyUniquePtr &&p) noexcept;
MyUniquePtr(const MyUniquePtr &p) = delete;
MyUniquePtr& operator=(const MyUniquePtr &p) = delete;
T* operator*() const noexcept {return mPtr;}
T& operator->()const noexcept {return *mPtr;}
explicit operator bool() const noexcept{return mPtr;}
void reset(T* q = nullptr) noexcept
{
if(q != mPtr){
if(mPtr)
delete mPtr;
mPtr = q;
}
}
T* release() noexcept
{
T* res = mPtr;
mPtr = nullptr;
return res;
}
T* get() const noexcept {return mPtr;}
void swap(MyUniquePtr &p) noexcept
{
using std::swap;
swap(mPtr, p.mPtr);
}
private:
T* mPtr;
};
template<typename T>
MyUniquePtr<T>& MyUniquePtr<T>::operator=(MyUniquePtr &&p) noexcept
{
swap(*this, p);
return *this;
}
template<typename T>
MyUniquePtr<T> :: MyUniquePtr(MyUniquePtr &&p) noexcept : mPtr(p.mPtr)
{
p.mPtr == NULL;
}
使用unique_ptr的示例,不能使用拷贝构造函数,可以使用移动拷贝构造函数,使用之前先把pa转为右值即可。
int main(){
unique_ptr<string> pa(new string("CHN"));
unique_ptr<string> pb(new string("USA"));
// pb=pa;//错误,不能使用拷贝赋值函数
// pb = move(pa); 正确
// pb = static_cast<unique_ptr<string>&&>((unique_ptr<string>&&)pa);正确
// pb = (unique_ptr<string>&&)pa;正确
unique_ptr<string>&& tt = (unique_ptr<string>&&)pa;
cout<<__LINE__<<*tt<<endl;//CHN
cout<<__LINE__<<*pa<<endl;//CHN
return 0;
}
其实目前我比较存疑的是,声明一个右值引用,指向pa这句代码是怎么执行的,貌似也没调用什么类的成员函数,g++ -S汇编看了一下,是下面这个样子,也没看出来什么特别之处,可能这句代码只是声明一个变量?希望有明白的大佬赐教。
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
.cfi_lsda 0x3,.LLSDA2299
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
pushq %r12 #
pushq %rbx #
subq $64, %rsp #,
.cfi_offset 12, -24
.cfi_offset 3, -32
movq %fs:40, %rax #, tmp154
movq %rax, -24(%rbp) # tmp154, D.49152
xorl %eax, %eax # tmp154
leaq -48(%rbp), %rax #, tmp112
movq %rax, %rdi # tmp112,
call _ZNSaIcEC1Ev #
leaq -48(%rbp), %r12 #, D.49146
movl $32, %edi #,
.LEHB3:
call _Znwm #
.LEHE3:
movq %rax, %rbx # tmp113, D.49147
movq %r12, %rdx # D.49146,
movl $.LC1, %esi #,
movq %rbx, %rdi # D.49147,
6.参考博客,以及更多引申内容
std::move()的实现,类型推导机制https://blog.csdn.net/p942005405/article/details/84644069
unique_ptr实现代码https://www.jianshu.com/p/77c2988be336
通用引用、完美转发https://www.jianshu.com/p/d19fc8447eaa
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)