听说还有人不懂右值和std::move()?

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

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页