【问题标题】:does std::vector copy/move elements when re-sizing?重新调整大小时 std::vector 是否复制/移动元素?
【发布时间】:2020-10-11 06:43:28
【问题描述】:

我正在使用移动控制器进行学习/刷新练习,但遇到了一些令我意想不到的事情。下面我有一个类person,其中包含一个std::string m_name;。我将其用作复制/移动 c'tors 的测试类。

这里是快速参考的代码:

#include <iostream>
#include <vector>

class person
{
public:
    std::string m_name;
    explicit person(const std::string &name) : m_name(name)
    {
        std::cout << "created " << m_name << std::endl;     
    }
    
    ~person()
    {
        std::cout << "destroyed " << m_name << std::endl;       
    }   
    
    person(const person &other) : m_name(other.m_name)
    {
        m_name += ".copied";
        std::cout << "copied " << other.m_name << " -> " << m_name << std::endl;
    }
    
    person(const person &&other) noexcept : m_name(std::move(other.m_name))
    {
        m_name += ".moved";
        std::cout << "moved " << other.m_name << " -> " << m_name << std::endl;
    }   
};

int main()
{
    std::vector<person> people;
    people.reserve(10);
    
    std::cout << "\ncopy bob (lvalue):" << std::endl;
    person bob{"bob"};
    people.push_back(bob);

    std::cout << "\nmove fred (lvalue):" << std::endl;
    person fred{"fred"};
    people.push_back(std::move(fred));

    std::cout << "\ntemp joe (rvalue):" << std::endl;
    people.push_back(person{"joe"});
    
    std::cout << "\nterminating:" << std::endl;
}

这给了我我期望的输出(主要是,除了为什么 std::string 内容没有“移动”?):https://godbolt.org/z/-J_56i

然后我删除 std::vector reserve 以便 std::vector 在我添加元素时必须“增长”。现在我得到了一些我真的没想到的东西:https://godbolt.org/z/rS6-mj

现在我可以看到 bob 被复制,然后在添加 fred 时移动,然后在添加 joe 时再次移动。我的印象是 std::vector 在必须重新分配空间时“移动”了。但我认为它执行的是内存复制/移动,而不是逐个对象的复制/移动。真没想到它会调用move构造函数。

现在如果我删除 move c'tor,我发现 bob 被复制了三遍!:https://godbolt.org/z/_BxnvU 这似乎效率很低。

来自 cplusplus.com:

push_back()

Add element at the end 在向量的末尾添加一个新元素, 在其当前的最后一个元素之后。 val 的内容被复制(或 移动)到新元素。

这实际上将容器大小增加了一倍,从而导致 当且仅当-时自动重新分配分配的存储空间 新的向量大小超过了当前向量的容量。

调整大小()

调整容器大小,使其包含 n 个元素。

如果 n 小于当前容器大小,则内容为 减少到它的前 n 个元素,删除那些超出的元素(并销毁 他们)。

如果 n 大于当前容器大小,则内容为 通过在末尾插入尽可能多的元素来扩展 n的大小。如果指定了 val,则新元素被初始化为 val 的副本,否则,它们是值初始化的。

如果 n 也大于当前容器容量,则自动 重新分配已分配的存储空间。

注意这个函数改变了容器的实际内容 通过插入或删除其中的元素。

我猜它并没有真正描述它“如何”重新分配,但是内存副本肯定是将向量移动到其新分配的内存空间的最快方法?

那么为什么在添加 std::vector 而不是内存副本时调用复制/移动 c'tors?

附注/问题:(任何可能这应该是一个单独的问题):亲自移动 c'tor 为什么打印 moved fred -&gt; fred.moved 而不是 moved -&gt; fred.moved。看来 std::string 移动分配并没有真正“移动”数据......

【问题讨论】:

  • but surely a memory copy is the fastest way to move the vector to its newly allocated memory space? 你不能只 memcpy C++ 对象。如果它们包含指向自身的指针怎么办?
  • 这是一个很好的观点,这意味着分配给向量可能会比我想象的更糟! - 特别是如果您创建的课程没有移动 c'tors :o。如果可以的话,我想总是保留一些空间:)
  • 这个问题可能很快就会得到解决:quuxplusone.github.io/blog/2018/07/18/…

标签: c++ c++11 vector move-semantics


【解决方案1】:

如果需要重新定位,将使用类似于std::move(xold.begin(), xold.end(), xnew.begin()); 的内容。它取决于值类型,并且向量通常会在自己的内部放置 new 。但它会移动,如果它可以移动。

你的移动构造函数

person(const person &&other) noexcept;

有一个缺陷:other 不应该是const,因为它必须被允许更改other窃取它的资源。在这个移动构造函数中

person(person&& other) noexcept : m_name(std::move(other.m_name)) {}

std::strings 自己的移动构造函数将执行类似于此的操作:

string(string&& other) noexcept : 
    the_size(other.the_size),
    data_ptr(std::exchange(other.data_ptr, nullptr))
{}

还需要添加移动赋值运算符:

person& operator=(person &&other) noexcept;

【讨论】:

  • 啊,谢谢现在对我来说很有意义:)。我没有添加移动分配以使其尽可能简单。重新 const'ing move c'tor - 它已经是 const 还是我在那里遗漏了什么?
  • @code_fodder 太棒了! :) 我添加了更多关于将其设为非const 的必要性。
  • 我误读了您的 const 评论。我从 move c'tor 中删除了 const(我以为你说需要添加 const),现在我从 fred 得到正确的输出:moved -&gt; fred.moved,之前是:moved fred -&gt; fred.moved,这意味着它实际上没有移动 - 这解决了那个小小的误会——谢谢!
  • @code_fodder 哦,我想念你误读了 :-) 太好了。不客气!
猜你喜欢
  • 2012-12-28
  • 2019-11-21
  • 2012-07-10
  • 1970-01-01
  • 2016-05-02
  • 2016-07-06
  • 1970-01-01
  • 1970-01-01
  • 2020-08-20
相关资源
最近更新 更多