【问题标题】:Why are my data elements being copied instead of moved?为什么我的数据元素被复制而不是移动?
【发布时间】:2020-06-04 04:19:53
【问题描述】:

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

鉴于模拟类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...

我在这里缺少什么?

【问题讨论】:

  • 附带说明,operator=(VecOfInt const&amp;) 应该在复制之前检查自分配,这不是正确的 delete'ing m_data。并且operator=(VecOfInt&amp;&amp;) 正在泄漏内存,因为m_data 指向的原始数组丢失了。您可以通过按值传递other 并让其构造函数在复制和移动操作之间做出决定,将两个operator= 组合成一个实现。并查看copy-swap idiom 以避免泄漏。
  • 另外,operator= 在分配新内存之前会改变*this 的状态,因此如果抛出异常,则您的对象已损坏。
  • 您应该将VecOfInt 更改为在内部使用std::vector&lt;int&gt;,然后所有这些问题都会消失。
  • 由于随着向量的增长,代码缺少v.reserve(3);,因此必须将对象复制到调整大小的向量。

标签: c++ c++11 stdvector move-semantics move-constructor


【解决方案1】:

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 的大小失败时,它会尝试使向量保持在尝试之前的状态。但是,如果移动操作在重新分配中途抛出,则没有安全的方法可以撤消已经成功进行的移动。他们也很可能会扔。最安全的解决方案是复制,如果移动可能会抛出。

【讨论】:

    【解决方案2】:

    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 {...}
    

    【讨论】:

    【解决方案3】:

    tl;dr:如果您的移动构造函数不是 noexceptstd::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;然后确保向量中有空间;然后将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 会随着您插入元素而逐渐增长 - 需要移动或复制构造。详情请见@NutCracker's post

    4。真正的问题是例外

    看到这个问题:

    How to enforce move semantics when a vector grows?

    std::vector 不知道它可以在调整大小时安全地移动元素 - 其中“安全地”意味着“没有例外”,因此它回退到复制。

    5。 “但我的复制 ctor 也会抛出异常!”

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

    【讨论】:

    • 实际上我不需要任何 ctor 或操作符,因为编译器已经为我实现了它。我只是在做一些测试。
    • @anc 由于m_data,编译器生成的不会为你工作。请参阅 3/5/0 的规则。
    【解决方案4】:

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

    像这样声明

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

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

    【讨论】:

      【解决方案5】:

      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;
      }
      

      如果对象的移动操作未使用 noexcept .,即不能保证他们不会扔。这就是为什么你应该致力于让你的移动操作(移动构造函数,移动赋值运算符)没有例外。这可以显着提高程序的性能。

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

      std::vector 充分利用了这个“如果可以移动,但复制如果 你必须”的策略,它不是标准中的唯一功能 图书馆。具有强大异常安全性的其他功能 C++98 中的保证(例如std::vector::reservestd::deque::insert、 等)行为相同。所有这些函数都替换了复制调用 仅当 C++98 中的操作调用移动 C++11 中的操作时 众所周知,移动操作不会发出异常。但是怎么可能 函数知道移动操作是否不会产生异常?这 答案很明显:它检查是否声明了操作 没有例外。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-06-29
        • 1970-01-01
        • 2017-11-07
        • 2015-02-26
        • 2011-04-07
        • 1970-01-01
        相关资源
        最近更新 更多