The idiomatic way to implement move-operations on classes with a standard container member can not be noexcept and therefore will not be movable by operations like vector.push_back(). Or am I mistaken?
To get speed from
vector<Elem> data;
// ...
data.push_back( elem );
We are encouraged to make out move operations noexcept -- so during the vectors resize the library can safely move elements to the reallocated storage.
class Elem {
// ...
Elem(Elem&&) noexcept; // noexcept important for move
Elem& operator=(Elem&&) noexcept; // noexcept important for move
};
So far so good, now my elems can be pushed-back much faster.
But: If I add a container as member, can my class be still be marked noexcept-move? All standard containers do not have their move noexcept!
class Stuff {
vector<int> bulk;
// ...
Stuff(Stuff&& o) // !!! no noexcept because of vector-move
: bulk(move(o.bulk))
{}
Stuff& operator=(Stuff&&) // !!! no noexcept...
{ /* appropriate implementation */ }
};
This also means, that we can also not rely on the compiler-generated move-operations, right? The following complete class will also not have noexcept-move-operations and therefore not be "fast", correct?
struct Holder {
vector<int> bulk;
};
Maybe vector<int> is a bit too simple to move, but what about vector<Elem>?
This would have great consequences on all data structures with containers as members.
Inheriting constructors and the implicitly-declared default constructors, copy constructors, move constructors, destructors, copy-assignment operators, move-assignment operators are all noexcept(true) by default, unless they are required to call a function that is noexcept(false) , in which case these functions are ...
noexcept is nice for two reasons: The compiler can optimize a little better because it doesn't need to emit any code for unwinding a call stack in case of an exception, and. It leads to incredible performance differences at runtime for std::vector (and other containers, too)
I feel your pain, really.
Some std::implementations will mark the move members of containers as noexcept, at least conditional on the allocator properties, as an extension. You can adapt your code to automatically take advantage of these extensions, for example:
class Stuff {
std::vector<int> bulk;
// ...
public:
Stuff(Stuff&& o)
noexcept(std::is_nothrow_move_constructible<std::vector<int>>::value)
: bulk(std::move(o.bulk))
{}
Stuff& operator=(Stuff&&)
noexcept(std::is_nothrow_move_assignable<std::vector<int>>::value)
{ /* appropriate implementation */ }
};
And you can even test whether or not your type does have noexcept move members:
static_assert(std::is_nothrow_move_constructible<Stuff>::value,
"I hope Stuff has noexcept move members");
static_assert(std::is_nothrow_move_assignable<Stuff>::value,
"I hope Stuff has noexcept move members");
libc++ in particular does have noexcept move members for all of its containers, whenever the allocator allows, and std::allocator always allows the container move members to be noexcept.
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