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
noexcept
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?
The relevant rule that vector
uses for inserting a single element at the end is:
noexcept
) then move. In this case insertion at the end of a vector provides the strong 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With