Move Semantics in C++

Last Updated: 2023-12-19 00:31:19 Tuesday

-- TOC --

前面总结过Copy Semantics,它实现了将一个对象所有资源copy一份到另一个对象的功能,结果是两个对象状态一致。但copy很多时候是低效的,C++同时还可以提供了Move Semantics,即将一个对象的所有资源,高效地move到另一个对象中去。

lvalue和rvalue

这部分叫做Value Category,现在我只能非常简单的理解一下。

lvalue:left value,可以简单理解为有名称的对象都是lvalue;变量都是lvalue;

rvalue:right value,可以简单理解为非lvalue;临时的,没名称的;literacy;

实现move操作的一般做法是,overload一个以rvalue为入参的constructor,程序员自己实现move逻辑。

C++语法规定,&表达的reference是lvalue,&&表达的reference是rvalue。

Move Constructor

下面这段测试代码,实现了一个move constructor:

#include <cstdio>
#include <cstring>
#include <utility>


struct box {
    char *p;

    box() = default;

    box(const char *content) {
        int len = strlen(content);
        if (len != 0) {
            p = new char[len+1]{};
            strcpy(p, content);
        }
    }

    void show() {
        printf("%s\n", p?p:"(null)");
    }

    // copy constructor
    box(const struct box& a) {
        printf("copy constructor\n");
        int len = strlen(a.p);
        if (len != 0) {
            p = new char[len+1]{};
            strcpy(p, a.p);
        }
    }

    // move constructor
    box(struct box&& a) noexcept {
        printf("move constructor\n");
        p = a.p;
        a.p = NULL;
    }

    ~box() {
        delete[] p;
    }
};


int main(void) {
    struct box a{"abcdefg"};
    struct box b{std::move(a)};
    b.show();
    a.show();
    return 0;
}

对象a是lvalue(有名字),通过std:move将其转换成rvalue,在构建对象b的时候,会调用move constructor。如果不使用std::move,直接传入对象a,此时应该是调用copy constructor,但是上面测试代码没有实现,编译器会报错。

关键词noexcept申明这个函数不会throw,资料上说,编译器常常会放弃调用没有noexcept申明的move constructor,转而使用copy constructor,据说copy更安全。具体参考:详解C++的异常处理

Move Assignment

下面是move assignment的代码:

    // move assignment
    struct box& operator=(struct box&& a) noexcept {
        if (this == &a)
            return *this;
        p = a.p;
        a.p = NULL;
        return *this;
    }
--snip--

int main(void) {
    struct box a{"abcdefg"};
    struct box b{};
    b = std::move(a);
    b.show();
    a.show();
    struct box c{std::move(b)};
    c.show();
    b.show();
    a.show();
    return 0;
}

下面是测试代码的运行结果:

$ g++ -Wall -Wextra test_move.cpp -o test_move
$ ./test_move
abcdefg
(null)
move constructor
abcdefg
(null)
(null)

重要的细节

对象被move之后,它拥有的资源就转移给其它对象了,逻辑上看,这个对象就不能再正常使用了!

更多lvalue和rvalue的测试

测试1:识别lvalue或rvalue

#include <cstdio>
#include <string>
using namespace std;


struct xyz {
    string what;

    xyz(const string& str) {
        printf("lvaule init\n");
        what = str;
    }

    xyz(const string&& str) {
        printf("rvalue init\n");       
        what = str;
    }
};


int main(void) {
    xyz x{"123123"}; // rvaule

    string s{"abcdefg"};
    xyz y{s};  // lvalue

    xyz z{string{"hahah..."}};  // rvalue

    string&& s2{"9999999"}; // syntax ok, but no use
    xyz x2{s2}; // lvalue

    return 0;
}

s2的申明是一种语法冗余,因为"9999999"是rvalue,那就不能用单个&,用两个或一个都不用,都OK,效果一样,得到的s2,是个lvalue

运行结果符合预期:

$ g++ -Wall -Wextra tr.cpp -o tr
$ ./tr
rvalue init
lvaule init
rvalue init
lvaule init

测试2:测试函数返回一个local object

#include <cstdio>
#include <string>
#include <iostream>
using namespace std;


struct xyz {
    xyz(): p{} {}

    xyz(const string& str) {
        p = new string{str};
    }

    void show() {
        cout << *p << endl;
    }

    xyz(xyz&& x) {
        printf("move constructor...\n");
        p = new string{*x.p};
        delete x.p;
        x.p = nullptr;
    }

    xyz& operator=(xyz& x) {
        printf("copy assignment...\n");
        delete p;
        p = new string(*x.p);
        delete x.p;
        x.p = nullptr;
        return *this;
    }

    xyz& operator=(xyz&& x) {
        printf("move assignment...\n");
        delete p;
        p = new string(*x.p);
        delete x.p;
        x.p = nullptr;
        return *this;
    }

    ~xyz() {
        delete p;
    }

private:
    string* p;
};


xyz create_xyz(void) {
    string s{"abcdefg"};
    xyz y{s};
    return y; // return a local xyz object.
}


int main(void) {
    xyz x;
    x = create_xyz();
    x.show();

    xyz y{std::move(x)};
    y.show();

    // The default copy constructor is deleted by default.
    //xyz z{x};
    //z.show();

    return 0;
}

由于y支持move,因此,move assignment is triggered by a returned local y from C++11。y在其scope内,是lvalue。可以总结概括一下:return a local lvalue which is move-capable is just OK!

运行结果:

$ g++ -Wall -Wextra tr.cpp -o tr
$ ./tr
move assignment...
abcdefg
move constructor...
abcdefg

这段测试代码,依然有谜题没有解开,如果写成xyz x{create_xyz()};,真不知道调用的是哪个constructor?打印p的地址看是copy,但copy constructor已经默认被delete了,显示地delete也是一样。打印对象地址,也是一样的...?

关于这个问题答案,参考:RVO和NRVO优化机制

std::move和std::swap

std::move将lvalue转换成rvalue,在头文件<utility>中。

The C++ committee probably should have named std::move as std::rvalue, but it’s the name we’re stuck with. The std:move function doesn’t actually move anything—it casts.

std::swap交换两个同类型对象所包含的所有属性。(在C语言中,我们常常实现swap,入参是指针,std::swap是升级版,是个template function,适合所有支持move的对象)

据说这是std::swap的源码:

template<typename _Tp>
    inline void
    swap(_Tp& __a, _Tp& __b)
#if __cplusplus >= 201103L
    noexcept(__and_<is_nothrow_move_constructible<_Tp>,
                is_nothrow_move_assignable<_Tp>>::value)
#endif
    {
      // concept requirements
      __glibcxx_function_requires(_SGIAssignableConcept<_Tp>)

      _Tp __tmp = _GLIBCXX_MOVE(__a);
      __a = _GLIBCXX_MOVE(__b);
      __b = _GLIBCXX_MOVE(__tmp);
    }

其中_GLIBCXX_MOVE()是一个宏定义#define _GLIBCXX_MOVE(__val) std::move(__val)

std::swap核心其实就三行代码,自己写可以这样(教科书中的写法):

template<typename T>
inline void swap(T &a,T &b) noexcept
{
    T temp = std::move(a);
    a = std::move(b);
    b = std::move(temp);
}

就是执行一次move constructor和两次move assignment。

关于inline

本文链接:https://cs.pynote.net/sf/c/cpp/202209011/

-- EOF --

-- MORE --