学习右值引用时,与 std::vector 同时使用时遇到的一些问题

注意
本文章内容已经过时,且包含本人当时的错误理解。

先简单说一下右值引用

右值引用的基础知识,在这篇文章中说的很清楚,建议仔细阅读。

https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/index.html

简单来说,右值主要实现了转移语义和完美转发。一个是避免了拷贝,一个是使得将一组参数原封不动的传递给另一个函数方便实现。

此文搁置的时间比较久了,现在把图片补齐重新发一下。

过程

我们容易写出以下简单的类:

class test {
   public:
    test() { puts("construct"); }
    test(const test& t) { puts("copy construct"); }

    test(test&& t) { puts("move construct"); }

    ~test() { puts("destruct"); }

    test& operator=(const test& t) {
        puts("assignment");
        return *this;
    }
    test& operator=(test&& t) {
        puts("move assignment");
        return *this;
    }
};

此类实现了最基本的默认构造函数、拷贝构造函数、移动构造函数、赋值运算符、移动赋值运算符。这里只是说明一下各函数声明。

将其丰富一下,让 test 类持有资源:

class test final {
   private:
    char* _data;
    size_t _len;

    void _init_data(const char* s) {
        _data = new char[_len + 1];
        memcpy(_data, s, _len);
        _data[_len] = 0;
    }

   public:
    test() : _data(nullptr), _len(0) { puts("construct"); }

    test(const char* s) {
        _len = strlen(s);
        _init_data(s);
    }

    test(const test& t) {
        _len = t._len;
        _init_data(t._data);
        puts("copy construct");
    }

    test(test&& t) {
        _len = t._len;
        _data = t._data;
        t._data = nullptr;
        puts("move construct");
    }

    ~test() {
        if (_data) {
            delete _data;
        }
        puts("destruct");
    }

    test& operator=(const test& t) {
        if (this != &t) {
            _len = t._len;
            _init_data(t._data);
        }
        puts("assignment");
        return *this;
    }
    test& operator=(test&& t) {
        if (this != &t) {
            _len = t._len;
            _data = t._data;
            t._data = nullptr;
        }
        puts("move assignment");
        return *this;
    }
};

初始化问题

如果我们初始化一个具有一个test实例的vector,有两种方法:

    // method A
    vector v{test()};

    // method B
    vector<test> v;
    v.push_back(test());

第一种使用 initializer_list ,第二种使用默认构造方法后 push_back 。

对于第一种,使用的构造函数为

vector( std::initializer_list<T> init,
 const Allocator& alloc = Allocator() );

这里会发生一次拷贝,具体原因我现在无法准确讲清楚。initializer_list 虽然按值传递,但在传入函数体过程中并不会发生深拷贝。

对于第二种,使用的 push_back  函数为

void push_back( T&& value );

因为重载了移动构造函数,这里直接调用它,不发生拷贝。

vector 扩容问题

为了方便观察拷贝次数,现在对代码做一点修改,使得拷贝时会在字符串后面附加一个下划线(不是一个好做法)。

#include <bits/stdc++.h>

using namespace std;

class test final {
   private:
    char* _data;
    size_t _len;

    void _init_data(const char* s) {
        _data = new char[_len + 10];
        memcpy(_data, s, _len);
        _data[_len] = 0;
    }

   public:
    test() : _data(nullptr), _len(0) { printf("construct: %s\n", _data); }

    test(const char* s) {
        _len = strlen(s);
        _init_data(s);
    }

    test(const test& t) {
        _len = t._len;
        _init_data(t._data);
        _data[_len++] = '_';
        printf("copy construct: %s\n", _data);
    }

    test(test&& t) {
        _len = t._len;
        _data = t._data;
        t._data = nullptr;
        printf("move construct: %s\n", _data);
    }

    ~test() {
        printf("destruct: %s\n", _data);
        if (_data) {
            delete _data;
        }
    }

    test& operator=(const test& t) {
        if (this != &t) {
            _len = t._len;
            _init_data(t._data);
        }
        puts("assignment");
        return *this;
    }
    test& operator=(test&& t) {
        if (this != &t) {
            _len = t._len;
            _data = t._data;
            t._data = nullptr;
        }
        puts("move assignment");
        return *this;
    }
};

int main() {
    vector<test> v;
    v.push_back(test("A"));
    v.push_back(test("B"));
    v.push_back(test("C"));
    return 0;
}

不难看出,此处的 vector 在扩容时疯狂进行拷贝。

为什么 vector 在此处扩容时会选择复制而不是移动呢?我们能不能让他移动?

考虑一下情景:假设我们使用移动策略,那么如果 vector 在扩容后,在移动的过程中移动构造函数抛出异常。此时问题将变得难以处理,原位置的资源已经被移动,而 vector 需要保证异常后还能维持数据不变。

解决方法也很简单,给移动构造函数加上 noexcept。注意,必须当此函数不可能抛出异常方可。

Ref: https://stackoverflow.com/questions/15730992/why-does-resize-cause-a-copy-rather-than-a-move-of-a-vectors-content-when-c

    test(test&& t) noexcept {
        _len = t._len;
        _data = t._data;
        t._data = nullptr;
        printf("move construct: %s\n", _data);
    }

    test& operator=(test&& t) noexcept {
        if (this != &t) {
            _len = t._len;
            _data = t._data;
            t._data = nullptr;
        }
        puts("move assignment");
        return *this;
    }

可以看到此时已不存在多余的拷贝。

CC BY-NC-SA 4.0 本作品使用基于以下许可授权:Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注