【问题标题】:Is there a benefit from noexcept move-operations when using containers in C++17?在 C++17 中使用容器时,noexcept 移动操作是否有好处?
【发布时间】:2017-02-04 05:54:32
【问题描述】:

在阅读 C++11 时,我有一种感觉,当使用标准容器(如 std::vector)和用户定义的数据类型时,鼓励提供 noexcept 移动操作,如果有的话,因为 then-and-只有这样容器才会在内部真正移动数据而不是复制。

今天尝试时,我发现 -std=c++1y(对于 C++14)和 g++-4.8 没有区别。也许我错过了规范中的更新,也许我的例子是错误的。

我比较了三个应该可以移动的数据结构的区别

  • 默认情况下可根据“零规则”移动
  • 通过提供移动操作可移动没有noexcept
  • 通过提供移动操作with可移动noexcept

框架:

#include <string>
#include <vector>
#include <chrono>
#include <iostream> // cout

using std::vector; using std::cout;    
using namespace std::chrono;

long long millisSeit(steady_clock::time_point start) {
    return duration_cast<milliseconds>(steady_clock::now()-start).count();
}

namespace {    
constexpr size_t ITERATIONS = 1000*1000;

template<typename ELEM>
void timeStuff(std::string name) {
    cout << name << "...";
    auto start = steady_clock::now();
    std::vector<ELEM> data{};
    for(size_t idx=0; idx<ITERATIONS; ++idx) {
        data.emplace_back( idx % 1719 );
    }
    cout << " " << millisSeit(start) << " ms" << std::endl;
}
}

使用我的三种数据类型:

struct RuleOfZeroVector {
    std::vector<int> val_;
    RuleOfZeroVector(int val) : val_(val, val) {}
};
struct MoveExceptVector {
    std::vector<int> val_;
    MoveExceptVector(int val) : val_(val, val) {}
    MoveExceptVector(MoveExceptVector&& o) /*noexcept*/ : val_{} { swap(val_, o.val_); }
    MoveExceptVector& operator=(MoveExceptVector&& o) /*noexcept*/ { swap(val_, o.val_); return *this; }
};
struct MoveNoExceptVector {
    std::vector<int> val_;
    MoveNoExceptVector(int val) : val_(val, val) {}
    MoveNoExceptVector(MoveNoExceptVector&& o) noexcept : val_{} { swap(val_, o.val_); }
    MoveNoExceptVector& operator=(MoveNoExceptVector&& o) noexcept { swap(val_, o.val_); return *this; }
};

并执行计时:

int main() {
    timeStuff<RuleOfZeroVector>("RuleOfZeroVector");
    timeStuff<MoveExceptVector>("MoveExceptVector");
    timeStuff<MoveNoExceptVector>("MoveNoExceptVector");
}

有了结果:

RuleOfZeroVector... 2461 ms
MoveExceptVector... 2472 ms
MoveNoExceptVector... 2468 ms

如您所见,没有真正的区别。

我预计MoveExceptVector 会比其他两个慢得多,因为我假设vector 在内部数据结构增长时会使用大量复制。错了吗?

【问题讨论】:

  • MoveExceptVector 添加一个复制ctor/assignment 操作符,你会看到区别。
  • vector::clear() 不需要移动数据。它只是运行每个元素的析构函数。 vector::push_backnoexcept搬家建设的主要受益人。
  • "g++-4.8 甚至。" 4.8 系列于 2013 年发布。那是很久以前的事了。
  • @towi :如果有相应的用户定义的移动特殊成员,则隐式生成复制特殊成员。如果您刚刚完成MoveExceptVector(MoveExceptVector const&amp;) = default;测试,您会发现差异很明显。 >_>
  • @T.C. : 确实,但如果我觉得有必要给出详细的迂腐解释,我会回答而不是评论。 ;-]

标签: c++ c++14 move-semantics c++17 noexcept


【解决方案1】:

vector用于在末尾插入单个元素的相关规则是:

  • 如果移动类型不能抛出(即移动操作标记为noexcept)然后移动。在这种情况下,在向量末尾插入可提供强大的异常安全保证。
  • 如果移动类型可能会抛出异常,并且它是可复制的,则改为复制它。这确保了如果抛出异常,我们没有移动一些原始对象,这将使原始对象处于未知状态。在这种情况下,您还可以获得强大的异常安全保证。
  • 否则,由于不可复制,您别无选择,必须移动。如果抛出异常,一些源对象可能已被移出,而有些则不会。在这种情况下,您只能获得基本的异常安全保证。

在您的示例中,对象不可复制,因为用户提供的移动构造函数和移动赋值运算符导致隐式复制构造函数被定义为已删除。所以vector别无选择。它必须使用移动操作,即使它们不是noexcept

如果您将类型设为可复制,您应该会看到不同之处,MoveExceptVector 的速度要慢得多,因为现在vector 可以选择执行可能抛出的动作或复制,因此它选择了复制。

struct MoveExceptVector {
    std::vector<int> val_;
    MoveExceptVector(int val) : val_(val, val) {}
    MoveExceptVector(MoveExceptVector&& o) /*noexcept*/ : val_{} { swap(val_, o.val_); }
    MoveExceptVector& operator=(MoveExceptVector&& o) /*noexcept*/ { swap(val_, o.val_); return *this; }
    // ADDED:
    MoveExceptVector(const MoveExceptVector&) = default;
};

如果对象不可复制,你不能指望它选择复制。

【讨论】:

    猜你喜欢
    • 2017-05-08
    • 2019-12-22
    • 2019-08-24
    • 2019-01-17
    • 2017-04-04
    • 2021-11-21
    • 1970-01-01
    • 2012-03-04
    相关资源
    最近更新 更多