为什么我的数据元素被复制而不是被移动?

Why are my data elements being copied instead of moved?

我正在执行一些关于移动语义的测试,我的 class 行为对我来说似乎很奇怪。

给定 mock class VecOfInt:

class VecOfInt {
public:
    VecOfInt(size_t num) : m_size(num), m_data(new int[m_size]) {}
    ~VecOfInt() { delete[] m_data; }
    VecOfInt(VecOfInt const& other) : m_size(other.m_size),  m_data(new int[m_size]) {
        std::cout << "copy..." <<std::endl;
        std::copy(other.m_data, other.m_data + m_size, m_data);
    }
    VecOfInt(VecOfInt&& other) : m_size(other.m_size) {
        std::cout << "move..." << std::endl;
        m_data = other.m_data;
        other.m_data = nullptr;
    }
    VecOfInt& operator=(VecOfInt const& other) {
        std::cout << "copy assignment..." << std::endl;
        m_size = other.m_size;
        delete m_data;
        m_data = nullptr;
        m_data = new int[m_size];
        m_data = other.m_data;
        return *this;
    }
    VecOfInt& operator=(VecOfInt&& other) {
        std::cout << "move assignment..." << std::endl;
        m_size = other.m_size;
        m_data = other.m_data;
        other.m_data = nullptr;
        return *this;
    }
private:
    size_t m_size;
    int* m_data;
};
  1. 好的案例

    我就地插入一个值时:

    int main() {
        std::vector<VecOfInt> v;
        v.push_back(10);
        return 0;
    }
    

    然后 它给了我以下输出 (我认为很好):

    move...

  2. 奇怪的案例

    我就地插入三个不同的值时:

    int main() {
        std::vector<VecOfInt> v;
        v.push_back(10);
        v.push_back(20);
        v.push_back(30);
        return 0;
    }
    

    然后输出调用拷贝构造函数3次:

    move... move... copy... move... copy... copy...

我在这里缺少什么?

std::vector 为其元素分配一块连续的内存。当分配的内存太短无法存储新元素时,分配一个新块并将所有当前元素从旧块复制到新块。

您可以使用 std::vector::reserve() 在添加新元素之前预先调整 std::vector 内存的容量。

尝试以下操作:

int main() {
    std::vector<VecOfInt> v;
    v.reserve(3);
    v.push_back(10);
    v.push_back(20);
    v.push_back(30);
    return 0;
}

你将得到:

move...
move...
move...

但是要让移动构造函数在重新分配时也被调用,您应该noexcept像:

VecOfInt(VecOfInt&& other) noexcept {...}

std::vector 在重新分配时不使用移动构造和移动分配,除非它们是 noexcept 或者如果没有复制选项。这是添加了 noexcept 的示例:

class VecOfInt {
public:
    VecOfInt(size_t num) : m_size(num), m_data(new int[m_size]) {}
    ~VecOfInt() { delete[] m_data; }
    VecOfInt(VecOfInt const& other) : m_size(other.m_size),  m_data(new int[m_size]) {
        std::cout << "copy..." <<std::endl;
        std::copy(other.m_data, other.m_data + m_size, m_data);
    }
    VecOfInt(VecOfInt&& other) noexcept : m_size(other.m_size) {
        std::cout << "move..." << std::endl;
        m_data = other.m_data;
        other.m_data = nullptr;
    }
    VecOfInt& operator=(VecOfInt const& other) {
        std::cout << "copy assignment..." << std::endl;
        m_size = other.m_size;
        delete m_data;
        m_data = nullptr;
        m_data = new int[m_size];
        m_data = other.m_data;
        return *this;
    }
    VecOfInt& operator=(VecOfInt&& other) noexcept {
        std::cout << "move assignment..." << std::endl;
        m_size = other.m_size;
        m_data = other.m_data;
        other.m_data = nullptr;
        return *this;
    }
private:
    size_t m_size;
    int* m_data;
};

现场live example输出:

move...
move...
move...
move...
move...
move...

这样做是为了保持异常安全。当调整 std::vector 失败时,它会尝试让矢量保持尝试前的状态。但是如果移动操作在重新分配的过程中抛出,则没有安全的方法来撤消已经成功执行的移动。他们也可以扔。最安全的解决方案是复制如果移动可能会抛出。

您的移动构造函数没有说明符 noexcept

像这样声明

VecOfInt(VecOfInt&& other) noexcept : m_size(other.m_size) {
    std::cout << "move..." << std::endl;
    m_data = other.m_data;
    other.m_data = nullptr;
}

否则 class 模板 std::vector 将调用复制构造函数。

tl;dr:如果您的移动构造函数不是 noexcept.

std::vector 将复制而不是移动

1。这与成员和动态分配无关

问题不在于您如何处理 foo 的字段。所以你的来源可能只是:

class foo {
public:
    foo(size_t num) {}
    ~foo() = default
    foo(foo const& other)  {
        std::cout << "copy..." <<std::endl;
    }
    foo(foo&& other) {
        std::cout << "move..." << std::endl;
    }
    foo& operator=(foo const& other) {
        std::cout << "copy assignment..." << std::endl;
        return *this;
    }
    foo& operator=(foo&& other) {
        std::cout << "move assignment..." << std::endl;
        return *this;
    }
};

你仍然得到相同的行为:try it

2。您确实看到的移动会分散注意力

现在,push_back() 将首先构造一个元素 - foo 在本例中;然后确保向量中有 space;然后 std::move() 它就位。因此,您的 3 个动作属于此类。让我们尝试改用 emplace_back(),它会在其位置构造矢量元素:

#include <vector>
#include <iostream>

struct foo { // same as above */ };

int main() {
    std::vector<foo> v;
    v.emplace_back(10);
    v.emplace_back(20);
    v.emplace_back(30);
    return 0;
}

这给了我们:

copy 
copy 
copy 

try it。所以这些动作真的只是分散注意力。

3。副本是由于矢量本身调整大小

您的 std::vector 随着您插入元素而逐渐增长 - 需要移动或复制构造。有关详细信息,请参阅

4。真正的问题是例外

看到这个问题:

How to enforce move semantics when a vector grows?

std::vector 不知道它可以在调整大小时安全地移动元素 - 其中 "safely" 表示 "without exceptions",因此它退回到复制。

5。 "But my copy ctor can throw an exception too!"

我想这是因为如果您在复制较小的缓冲区时遇到异常 - 您仍然没有触及它,那么至少您的原始未调整大小的向量是有效的并且可以使用。如果您开始 移动 元素并遇到异常 - 那么您无论如何都没有该元素的有效副本,更不用说有效的较小向量了。

noexcept 装饰器添加到您的移动构造函数和移动赋值运算符:

VecOfInt(VecOfInt&& other) noexcept : m_size(other.m_size) {
    std::cout << "move..." << std::endl;
    m_data = other.m_data;
    other.m_data = nullptr;
}

VecOfInt& operator=(VecOfInt&& other) noexcept {
    std::cout << "move assignment..." << std::endl;
    m_size = other.m_size;
    m_data = other.m_data;
    other.m_data = nullptr;
    return *this;
}

一些函数(例如 std::move_if_noexcept,被 std::vector 使用)将决定 复制 你的对象,如果它的移动操作没有用 noexcept.,即不能保证它们不会抛出。这就是为什么您的目标应该是使您的移动操作(移动构造函数、移动赋值运算符)不例外。这可以显着提高程序的性能。

根据 Scott Meyer 在 Effective Modern C++ 中的说法:

std::vector takes advantage of this "move if you can, but copy if you must" strategy , and it's not the only function in the Standard Library that does. Other functions sporting strong exception safety guarantee in C++98 (e.g. std::vector::reserve, std::deque::insert, etc) behave the same way. All these functions replace calls to copy operations in c++98 with calls to move operations in C++11 only if the move operations are known not to emit exceptions. But how can a function know if a move operation won't produce an exception? The answer is obvious: it checks to see if the operation is declared noexcept.