C++ 拷贝控制:对象移动

关于rvalue-reference&move
http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html
关于copy and swap :
http://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom
http://www.cppsamples.com/common-tasks/copy-and-swap.html
Effective modern C++
https://doc.lagout.org/programmation/C/Meyers.Effective.Modern.C++.en.pdf

左值和右值

一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。

首先要知道什么是左值、什么是右值。C++ Primer 5th P121 4.1.1有说明。

TODO:补充

右值引用

新标准中的一个最主要的特性是可以移动而非拷贝对象的能力。在某些情况下,对象拷贝后就立即被销毁了,此时使用移动而非拷贝将大幅提升性能。P470 13.6

上面是引入“移动”的原因。“某些情况”是指:

  • 类似书中StrVec类,重新分配内存时拷贝是不必要的,更好的方式是移动;
  • 某些类包含不能共享的资源(如指针和IO缓冲),包括IO类和unique_ptr;

就版本的标准库中,容器中保存的类必须是可拷贝的。但在新版本中,可以用容器保存不可拷贝的类型,只要它们能被移动即可。
这里写图片描述

为了支持移动操作,新标准引入了“右值引用”。

类似任何引用,一个右值引用不过是某个对象的另一个名字而已。( 引自 C++ Primer 5th P471 13.6.1)

左值持久、右值短暂。

右值要么是字面常量,要么是表达式求值过程中创建的临时对象。可以说右值引用只能绑定到临时对象,因此使用右值引用的代码可以接管引用的对象的资源,这里体现了“移动”。(意思就是:反正临时对象最终被销毁,那么我们可以直接“移动”临时对象的资源到我们的对象中,从而避免了不必要的拷贝。看上去好像器官捐献 >_<)

变量是左值

这意味着,一个右值引用本身是一个左值,因为右值引用本身是变量啊。>_<

std::move

虽然不能直接将一个右值引用直接绑定到一个左值上,但是可以显示的将一个左值转换为对应的右值引用类型。还可以用std::move将一个左值转换为对应的右值引用类型。

int &&r1 = 0x10;
int &&r2 = r1; // error, r1 is lvalue
int &&r3 = std::move(r1); // ok, use std::move
int &&r4 = (int&&)r1; // ok, cast directly

调用了std::move意味着做了一个承诺:处理对r1赋值或销毁它外,我们将不再使用它!(自己在实现类的移动操作时,记得将被窃取资源的对象置于一种正确的可析构的状态,避免重复释放资源)

注意:

Names of rvalue reference variables are lvalues and have to be converted to xvalues to be bound to the function overloads that accept rvalue reference parameters, which is why move constructors and move assignment operators typically use std::move:

// Simple move constructor
A(A&& arg) : member(std::move(arg.member)) // the expression "arg.member" is lvalue
{} 
// Simple move assignment operator
A& operator=(A&& other) {
     member = std::move(other.member);
     return *this;
}

One exception is when the type of the function parameter is rvalue reference to type template parameter (“forwarding reference” or “universal reference”), in which case std::forward is used instead.

理解这一点很重要,关键要记住,右值引用本身是一个左值,必要时要使用std::move(在move ctor,move assign中通常用到它,handy网络库的ExitCaller也是这么用的)

一个例外情况是,对于模板参数中的右值引用,需要使用std::forward

移动构造函数 & 移动赋值运算符

移动构造函数

C++ 拷贝控制:对象移动_第1张图片

注意一下几点:

  • 当编写的移动操作不抛出异常时,请使用noexcept,它通知标准库我们的构造函数不抛出异常。否则,标准库会认为移动我们的对象可能抛出异常,为了处理这种可能新而做一些额外的工作。P474给出了详细的原因。
  • 直接接管资源
  • 让被接管对象运行析构是安全的

移动赋值运算符

StrVec & operator=(StrVec &&rhs) noexcept
{
    if(this != &rhs) { // 防止自赋值
        // 释放已有资源
        free(); 
        // 接管已有资源
        elements = rhs.elements;
        first_free = rhs.first_free;
        cap = rhs.cap;
        //将rhs置于可析构状态
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
}

除了上面几点,赋值操作需要:

  • 处理自赋值
  • 保证异常安全
  • 返回引用

移动赋值操作符处理自赋值一开始觉得挺奇怪的。因为=右侧是一个右值,而=左侧是一个左值,怎么会是同一个对象呢? 原因是:=右侧可能是使用std::move返回的结果,此时有可能是同一个对象啦。

合成的移动操作

13.6 P475 说明了此情况。
C++ 拷贝控制:对象移动_第2张图片
这里写图片描述

使用移动还是拷贝

如果一个类既有拷贝构造函数,也有移动构造函数,那么编译器将使用普通的函数匹配规则来确定使用哪个构造函数。即:移动右值,拷贝左值

注意:一个右值可以匹配移动构造函数(T&&)和拷贝构造函数(const T&)(const左值引用可以绑定右值)。但是移动构造函数是精确匹配。

如果一个类没有移动构造函数,右值也被拷贝

这里写图片描述

Copy and Swap

13.3 P459 这种技术可以自动处理自赋值的情况,且是异常安全的。

参数的传递方式是:值传递。如果传递过来的是一个左值,将调用HasPtr的拷贝构造函数构造rhs;如果rhs是一个右值,将调用HasPtr的移动构造函数构造rhs。随后将rhs与this对象进行交换,离开作用域后,rhs被析构。

这样,一个operator=就整合了拷贝赋值操作符和移动赋值操作符。能够处理自赋值并且是异常安全的!

    // copy assign: copy and swap
    HasPtr& operator=(HasPtr rhs) {
      std::cout << "copy assign" << std::endl;
      swap(*this, rhs);
      return *this;
    }

    void swap(HasPtr &lhs, HasPtr &rhs) {
      std::swap(lhs.ps, rhs.ps);
      std::swap(lhs.i, rhs.i);
    }

五个拷贝控制成员

包括:copy ctor、copy assign、move ctor、move assign、dtor。
这里写图片描述
C++ 拷贝控制:对象移动_第3张图片

HasPtr 类

class HasPtr {
public:
    // default ctor
    HasPtr(const string &s = string()):
        ps(new string(s)), i(0) {
          std::cout << "default ctor" << std::endl;
        }

    // copy ctor
    HasPtr(const HasPtr &p) :
        ps(new string(*p.ps)), i(p.i) { 
          std::cout << "copy ctor" << std::endl;
        }

    // move ctor
    HasPtr(HasPtr &&p) noexcept:
        ps(p.ps), i(p.i) {
          std::cout << "move ctor" << std::endl;
          p.ps = nullptr;
          p.i = 0;
    }

    // copy assign: copy and swap
    HasPtr& operator=(HasPtr rhs) {
      std::cout << "copy assign" << std::endl;
      swap(*this, rhs);
      return *this;
    }

    void swap(HasPtr &lhs, HasPtr &rhs) {
      std::swap(lhs.ps, rhs.ps);
      std::swap(lhs.i, rhs.i);
    }

    // copy assign
    //HasPtr & operator= (const HasPtr &rhs) {
    //    // 可以防止自拷贝时出现错误
    //    auto newp = new string(*rhs.ps);
    //    delete ps;
    //    ps = newp;
    //    i = rhs.i;
    //    return *this;
    //}
    ~HasPtr() { delete ps; }
private:
    string *ps;
    int i;
};

StrVec 类

// 实现类似Vector的容器,但是只可以用string作为元素

class StrVec{
public:
    StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) { }
    StrVec(const StrVec&);
    StrVec &operator=(const StrVec &);
    ~StrVec();
    void push_back(const string &);
    size_t size() const { return first_free - elements; }
    size_t capacity() const { return cap - elements; }
    string *begin() const { return elements; }
    string *end() const { return first_free; }
private:
    static allocator<string> alloc;
    void chk_n_alloc() {
        if(size() == capacity())
            reallocate();
    }
    pair<string*, string*> alloc_n_copy(const string*, const string *);
    void free();
    void reallocate();
    string *elements;
    string *first_free;
    string *cap;
};
allocator<string> StrVec::alloc;
void StrVec::push_back(const string &s)
{
    chk_n_alloc();
    alloc.construct(first_free++, s);
}

pair<string*, string*>
StrVec::alloc_n_copy(const string *b, const string *e)
{
    auto data = alloc.allocate(e - b);
    return {data, uninitialized_copy(b, e, data)}; // 初始化列表,pair->first 是内存的起始地址,pair->second是最后一个内存的后面位置
}

void StrVec::free()
{
    if(elements) {
        for(auto p = first_free; p != elements; )
            alloc.destroy(--p); // 调用string的析构函数
        alloc.deallocate(elements, cap - elements); // 释放空间
    }
}

StrVec::StrVec(const StrVec &s)
{
    auto newdata = alloc_n_copy(s.begin(), s.end());
    elements = newdata.first;
    cap = first_free = newdata.second;
}

StrVec::~StrVec() { free(); }

StrVec & StrVec::operator=(const StrVec &s)
{
    auto newdata = alloc_n_copy(s.begin(), s.end()); // 先创建新内存,可以正确处理自赋值的情况
    free();
    elements = newdata.first;
    cap = first_free = newdata.second;
    return *this;
}

void StrVec::reallocate()
{
    auto newcapacity = size() ? 2 * size() : 1; // 容量扩大一倍
    auto newdata = alloc.allocate(newcapacity);

    auto dest = newdata;
    auto elem = elements;
    for(size_t i = 0; i != size(); ++i)
        alloc.construct(dest++, std::move(*elem++)); // 调用std::move的返回的结果(一个右值引用)会令construct使用string的移动构造函数(参数为右值引用)!
    free();

    elements = newdata;
    first_free = dest;
    cap = elements + newcapacity;
}

int main()
{
    StrVec sv;
    sv.push_back("hong");
    sv.push_back("jin");
    cout << *sv.begin() << endl; // hong
    cout << *(sv.begin() + 1) << endl; //jin
    cout << sv.size() << " " << sv.capacity() << endl; // 2 2
    sv.push_back("jin");
    cout << sv.size() << " " << sv.capacity(); // 3 4
    return 0;
}

你可能感兴趣的:(C++)