玩命加载中 . . .

13.6-对象移动


13.6 对象移动

有时候对象拷贝之后就立即销毁了,如果只是移动而不是拷贝对象会大幅度提升性能,因为移动不用重新分配内存和拷贝数据

  • 标准库容器、stringshared_ptr类既支持移动也支持拷贝,IO类和unique_ptr类可以移动但不能拷贝

13.6.1 右值引用

右值引用只能绑定到一个将要销毁的对象,可以自由地将一个右值引用的资源移动到另一个对象中

对于常规的左值引用,不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用则相反,但不能直接绑定到一个左值上

int i = 10;
int &r1 = i;            // OK, 左值引用
int &&r2 = i;           // Err, 不能将右值引用绑定到左值
int &r3 = i * 10;       // Err, i*10是一个右值
const int &r3 = i * 10; // OK, 可以将const引用绑定到右值
int &&r4 = i * 10;      // OK, 右值引用
cout << r4 << endl;     // 100
r4 += 10;               // OK,也可以修改
  • 返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是左值
  • 返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值
int& func1(int& num) {
    num = num * num;
    return num;
}

int func2(int num) {
    return num * num;
}

int main(int argc, char const *argv[]) {
    int a = 5;
    int &ref1 = func1(a);   // 返回左值
    cout << ref1 << endl;   // 25

    int b = 4;
    int &&ref2 = func2(b);  // 返回右值
    cout << ref2 << endl;   // 16

    const int &ref3 = func2(b); // const引用可以绑定右值
    cout << ref3 << endl;       // 16

    return 0;
}

右值引用指向将要被销毁的对象,所以,我们可以从绑定到右值引用的对象“窃取”状态

变量是左值
int &&r1 = 10;
int &&r2 = r1;  // Err, r1是左值,尽管r1是右值引用,但还是一个变量

std::move()

可以显式地将一个左值转换为对应的右值引用类型
可以用std::move函数来获得绑定到左值上的右值引用

int &&r1 = 10;
int &&r2 = std::move(r1);   // OK

调用std::move()之后,除了对r1赋值或销毁外,不能再使用它

13.6.2 移动构造函数和移动赋值运算符

移动构造函数

移动构造函数的参数是右值引用,新对象接管源对象的资源之后,把源对象的指针置空,使其成为可析构的状态

  • 必须把源对象指针置空,不然源对象移动完就析构,移动的资源也跟着释放了
class Person
{
private:
    string *ptr;
    int age;

public:
    Person(const string &s = string()) : ptr(new string(s)), age(0) {}
    Person(const Person &p) : ptr(new string(*p.ptr)), age(p.age) {}

    Person(Person &&p) noexcept // 移动操作不应抛出异常
        : ptr(p.ptr), age(p.age) {
        cout << "move constructor" << endl;
        p.ptr = nullptr; // 源对象的指针置空
    }

    ~Person() {
        delete ptr;
    }
};

int main(int argc, char const *argv[]) {
    Person p1;
    Person p2 = std::move(p1);
    return 0;
}
move constructor

移动赋值运算符

先释放自身的资源,接管源对象的资源,再把源对象的指针置空

class Person {
private:
    string *ptr;
    int age;

public:
    Person& operator=(Person &&rhs) noexcept {
        cout << "move copy assignment" << endl;
        if (this != &rhs) {
            delete ptr;     // 释放拥有的资源
            ptr = rhs.ptr;
            age = rhs.age;
            rhs.ptr = nullptr;
        }
        return *this;
    }
};

Person func(Person& p) {    // 函数返回右值
    return p;
}

int main(int argc, char const *argv[]) {
    Person p1, p2;
    p2 = func(p1);
    return 0;
}
move copy assignment

如果只定义拷贝构造函数,没有定义移动构造函数,那也可以用右值进行构造,调用的是拷贝构造函数

拷贝并交换赋值运算符和移动操作

可以将拷贝赋值运算符和移动赋值运算符统一起来,调用自定义swap函数即可

class Person
{
private:
    string *ptr;
    int age;

public:
    Person(const string &s = string()) : ptr(new string(s)), age(0) {}
    Person(const Person &p) : ptr(new string(*p.ptr)), age(p.age) {
        cout << "copy constructor" << endl;
    }    // 拷贝构造函数

    Person(Person &&p) noexcept // 移动构造函数
        : ptr(p.ptr), age(p.age)
    {
        cout << "move constructor" << endl;
        p.ptr = nullptr; // 源对象的指针置空
    }

    Person& operator=(Person rhs) {
        cout << "swap copy assignment" << endl;
        swap(*this, rhs);
        return *this;
    }
    friend void swap(Person&, Person&);
    ~Person() { delete ptr; }
};

inline
void swap(Person& lhs, Person& rhs) {
    using std::swap;
    swap(lhs.ptr, rhs.ptr);
    swap(lhs.age, rhs.age);
}

int main(int argc, char const *argv[])
{
    Person p1("kavin"); // 直接初始化
    Person p2;
    p2 = p1;
    cout << "----------" << endl;
    p2 = std::move(p1);
    return 0;
}
// copy constructor 左值调用拷贝构造函数
// swap copy assignment
// ----------
// move constructor 右值调用移动构造函数
// swap copy assignment

在拷贝赋值运算符中调用自定义的swap函数,注意参数是非引用的,所以,当实参是左值时,调用拷贝构造函数,当实参是右值时,调用移动构造函数

13.6.3 右值引用和成员函数

可以同时定义拷贝和移动版本的成员函数

void push_back(const string&);
void push_back(string&&);

当调用push_back时,实参类型决定了新元素是拷贝还是移动

StrVec vec;
string s = "hello";
vec.push_back(s);       // 调用push_back(const string&)
vec.push_back("done");  // 调用push_back(string&&)

右值和左值引用成员函数

可以给成员函数增加引用限定符,强制左侧运算对象是一个左值/右值

class Person
{
private:
    string *ptr;
    int age;
public:
    Person& operator=(const Person&) &;
};
Person& Person::operator=(const Person& rhs) &
{
    // 执行将rhs赋予本对象的工作
    return *this;
}
如果同时使用const和引用限定,const必须排在前面

重载和引用函数

可以综合引用限定符和const来区分一个成员函数的重载版本

class Foo
{
private:
    vector<int> data;
public:
    Foo sorted() &&;        // 可用于可改变的右值
    Foo sorted() const &;   // 可用于任何类型的Foo
};

// 本对象为右值,因此可以原址排序
Foo Foo::sorted() && 
{
    sort(data.begin(), data.end());
    return *this;
}

// 本对象是const或左值,不能对其进行原址排序
Foo Foo::sorted() const & 
{
    cout << "left value" << endl;
    Foo ret(*this);     // 先拷贝对象
    sort(ret.data.begin(), ret.data.end());
    return ret;
}

对象是一个右值,意味着没有其他用户,因此可以改变对象
当对一个const右值或一个左值执行sorted时,不能改变对象,因此需要在排序前拷贝对象

如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符

文章作者: kunpeng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kunpeng !
  目录