【问题标题】:How to enable move semantics when adding custom objects to a vector?将自定义对象添加到向量时如何启用移动语义?
【发布时间】:2019-05-03 07:49:51
【问题描述】:

以下代码将包含大向量的对象传递到向量中。我希望这是高性能的。在对push_back 的调用中,我是否需要将test 转换为右值?我是否需要告诉编译器如何移动 struct Test 的实例?还是这一切都会自动进行?

int main()
{
    struct Test
    {
        std::vector<size_t> vals;
        double sum;
    };
    std::vector<Test> vecOfTest;
    vecOfTest.reserve(100000);

    for (size_t i = 0; i < 100000; i++)
    {
        Test test{};
        test.vals.reserve(i);
        for (size_t j = 0; j < i; j++)
        {
            test.vals.push_back(j);
            test.sum += j;
        }
        vecOfTest.push_back(test);
    }


    return 0;
}

【问题讨论】:

  • 只要做vecOfTest.emplace_back(std::move(test));,你应该会很好。编译器会为 Test 自动生成一个适当的移动构造函数,因为它的所有成员都是微不足道的或者有一个移动构造函数。
  • @Swordfish 标准不允许这样做,因此如果这导致可观察到的差异,则不允许编译器这样做。
  • @Swordfish 大多数编译器认为正常的内存分配是一种可观察到的副作用(涉及系统调用)。
  • @Swordfish msvc/gcc 不合理,那么呢?
  • @MaxLanghof 如果您推送或放置一个已经构建的对象,它不应该有任何的区别。如果您有 SomeClass sc(7, 10, 12); v.push_back(std::move(sc));v.emplace_back(7, 10, 12),则临时保存。

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


【解决方案1】:

我希望它是高性能的

以下应该足够了。希望cmets能帮助你理解代码。

#include <vector>
#include <iostream>
#include <numeric>

struct Test
{
    std::vector<size_t> vals;
    double sum = 0; // initialing is a good idea
    Test(const size_t v, const double res) // provide constructor(appropriate one)
        : vals(v), // tell the size of the vals directly in the constructor
          sum(res) 
    {}
};

int main()
{

    std::vector<Test> vecOfTest;
    vecOfTest.reserve(100000);

    for (size_t i = 0; i < 100000; i++)
    {
        // Test& last_test = vecOfTest.emplace_back() needs C++17, otherwise
        // use std::vector::back()
        auto& last_test = vecOfTest.emplace_back(   // create the Test object in place and take the reference to it
            i,                     // tell the size of vals in newly creating Test object
            ((i - 1) * i) / 2.0    // ((j-1) * j)/2 = sum from 0 to j-1
        );
        std::iota(std::begin(last_test.vals), std::end(last_test.vals), static_cast<size_t>(0)); // populate, 0 to size of vals
    }
    return 0;
}

【讨论】:

  • 最好先将对象放在vecOfTest 中,然后直接填充对象的向量...
  • * 0.5:你不必要地引入了浮点运算。如果不需要,请不要这样做,只需使用/ 2
  • @Aconcagua 老实说,我没有得到你的第一点。 OP做的不一样吗?
  • @Aconcagua ((i - 1) * i)/2 可能会在手动总和没有溢出的情况下溢出...此外,原始代码 确实 累积在 double 变量中,因此使用总和公式实际上并没有“引入”浮点运算。乘以 0.5 与浮点数学一样好。
  • @MaxLanghof 不是我来介绍乘法的;但正如在size_t 上所做的那样,无论使用*.5 还是/2 之后,它都会溢出!如果真的发生了溢出,那么 double 值中的总和无论如何都是不准确的(double 提供的精度低于 64 位 size_t ——假设我们有这样的)。要解决任何此类问题:uint64_t n = i; sum = n&amp;1 ? (n - 1) / 2 * n : n/2*(n-1);。如果n 确实太大,我们可能会添加额外的溢出检测(sum &lt; n 可能会这样做,但需要进一步评估);在没有进一步检测的情况下,这将比不精确(双)值好得多!
【解决方案2】:

您的Test 结构没有定义任何特殊的成员函数(复制构造函数、析构函数等),这意味着默认的移动赋值运算符和默认的移动复制构造函数是自动生成的,它们将移动每个数据成员结构。所以Test 是一个可移动类型,并且它从中受益,因为vector&lt;size_t&gt; 是一个可移动数据成员。

但是,移动不会自动执行,因为从一个对象移动会改变它。即使你会这样认为:

    vecOfTest.push_back(test);
}

会做一个隐式移动,因为范围结束,它不会。隐式移动会使编译器和程序员都陷入困境。编译器需要证明使test 无效是可以的。程序员将需要不断地调查是否需要显式移动,而最终结果将是无论如何都只进行显式移动。所以出于这个原因,不会发生隐式移动(但请参阅下面的规则例外情况。)您需要自己做:

vecOfTest.push_back(std::move(test));

唯一不需要移动的情况是移动会干扰省略。例如,在返回 Test 的函数中,这样:

Test test;
return std::move(test);

会移动,但最好不要移动。最好:

return test;

相反。这不是一个隐含的举动。这是一个省略号。省略比移动快,移动可以防止省略。但是,在无法省略的情况下,将执行隐式移动。这是我所知道的唯一一种隐式移动会发生的情况:作为省略的替代品。您的原始代码:

vecOfTest.push_back(test);

不是省略的情况,因此永远不会发生隐式移动。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-09-06
    • 1970-01-01
    • 2014-10-24
    • 2012-12-01
    • 1970-01-01
    • 2023-03-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多