9

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.

David G
  • 94,763
  • 41
  • 167
  • 253
towi
  • 21,587
  • 28
  • 106
  • 187
  • Part of the problem is the unknowns of allocator traits. You would think that `std::vector::swap()` could be `noexcept` as well. – woolstar Jan 23 '14 at 22:17
  • Even though the standard container's member functions have no required noexcept-specification, they have exception guarantees. If those guarantees say that operation does not throw, you can make your own move ctor noexcept "safely". – dyp Jan 23 '14 at 23:23

2 Answers2

10

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.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • Ok, thanks. What about what the compiler generates for `struct Holder;`? I figure that class will have an unefficient move, ie. a copying one? The compiler will not include an conditional `noexcept`, I presume. (And if I were to continue Scott's joke, I would ask if there is a downloadable binary for [Windows](http://libcxx.llvm.org/results.Windows.html), yet. But I won't ;-) – towi Jan 24 '14 at 08:40
  • 1
    Actually the `noexcept` spec for `Holder` will be exactly as I've recommended you explicitly do for `Stuff`. Indeed, you could also just use `= default`. And you can test `Holder`'s `noexcept` spec in the exact same manner I've shown for `Stuff`. Specifically, if the move constructor for `vector` is `noexcept`, then the implicit move constructor of `Holder` will also be `noexcept`. – Howard Hinnant Jan 24 '14 at 15:42
  • I am relieved. This is what [§12.9](http://stackoverflow.com/a/18654089/472245) states with its many words, I gather. – towi Jan 24 '14 at 16:23
  • @HowardHinnant The test might be a bit misleading because it can also catch `noexcept` copy constructors; see comments at https://stackoverflow.com/a/18654089/1046167 – Louis Semprini Sep 24 '20 at 19:37
  • If the copy constructor is to be used to do the move, the `is_nothrow_move_` traits are still the right tool to ask the question (by design). – Howard Hinnant Sep 24 '20 at 21:02
  • The same issue confused the committee a decade ago and was fixed by renaming the traits by this paper: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3142.html – Howard Hinnant Sep 24 '20 at 21:07
2

It depends on the allocator that your standard container is using. The default allocator std::allocator<T> is guaranteed not to throw on copy (and has no move constructor), which in turn means that the container will not throw on move.

One interesting feature of noexcept compared with the deprecated throw() is that you can use an expression that is evaluated at compile time. The exact condition to be tested might not be trivial though...

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489