在C++ 17中使用容器时,noexcept移动操作是否有好处?

tow*_*owi 6 c++ move-semantics noexcept c++14 c++17

在阅读关于C++ 11时,我感觉当使用标准容器(如std::vector)和用户定义的数据类型时,鼓励人们提供noexcept移动操作,如果有的话,因为那时候容器会在内部真正移动数据而不是复制.

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

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

  • 按照"零度规则"默认可移动
  • 可以通过提供没有的移动操作来移动 noexcept
  • 可通过提供移动操作移动 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;
}
}
Run Code Online (Sandbox Code Playgroud)

使用我的三种数据类型:

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; }
};
Run Code Online (Sandbox Code Playgroud)

并执行时间:

int main() {
    timeStuff<RuleOfZeroVector>("RuleOfZeroVector");
    timeStuff<MoveExceptVector>("MoveExceptVector");
    timeStuff<MoveNoExceptVector>("MoveNoExceptVector");
}
Run Code Online (Sandbox Code Playgroud)

结果如下:

RuleOfZeroVector... 2461 ms
MoveExceptVector... 2472 ms
MoveNoExceptVector... 2468 ms
Run Code Online (Sandbox Code Playgroud)

如你所见,没有真正的区别.

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

Jon*_*ely 4

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;
};
Run Code Online (Sandbox Code Playgroud)

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