Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a benefit from noexcept move-operations when using containers in C++17?

When reading about C++11 I had the feeling that when using the standard containers (like std::vector) with user defined datatypes one was encouraged to provide noexcept move operations, if at all, because then-and-only-then the containers would internally really move data around instead of copying.

When trying that out today I saw no difference with -std=c++1y (for C++14) and g++-4.8 even. Maybe I missed an update in the spec, maybe my example is wrong.

I compared three data structures that should be moveable with the differences

  • moveable by default by following the "Rule Of Zero"
  • moveable by providing move-ops without noexcept
  • moveable by providing move-ops with noexcept

Framework:

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

With my three datatypes:

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

And executing the timings:

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

With the results:

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

As you can see, no real difference.

I expected MoveExceptVector to be much slower then the other two, because I assumed vector will use a lot of copying when the internal data-structure grows. Wrong?

like image 224
towi Avatar asked Sep 26 '16 19:09

towi


1 Answers

The relevant rule that vector uses for inserting a single element at the end is:

  • If moving the type can't throw (i.e. move operations are marked noexcept) then move. In this case insertion at the end of a vector provides the strong exception-safety guarantee.
  • If moving the type could throw, and it's copyable, then copy it instead. This ensures that if an exception is thrown we haven't moved some of the original objects, which would leave the original object in an unknown state. In this case you also get the strong exception-safety guarantee.
  • Otherwise, since it's not copyable, you have no choice, you must move. If an exception is thrown some of the source objects might have been moved-from, and some won't be. In this case you only get the basic exception-safety guarantee.

In your example the objects are not copyable, because the user-provided move constructors and move assignment operators cause the implicit copy constructor to be defined as deleted. So vector has no choice. It must use the move operations even though they're not noexcept.

If you make the types copyable you should see a difference, with MoveExceptVector being much slower, because now vector has a choice between doing moves that could throw or copying, so it chooses copying.

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

You can't expect it to choose copying if the object isn't copyable.

like image 75
Jonathan Wakely Avatar answered Sep 17 '22 21:09

Jonathan Wakely