11

While explaining move operations on objects with a colleague, I basically said that move operations should not throw exceptions in a container because if the move operation fails, then there is no way to bring back the original object reliably. Thinking about this more, I'm wondering if that is not correct and that if a move operation that does throw, it could revert the original object back to it's original state.

The reason for this, is that if an object can throw, then it would throw not due to copying or moving the contained objects from the old to the new address, but throw if a resource failed to be acquired. So all of the original information should still be there. If this is the case, then should the compiler not be able to reverse the operations that it did to reconstitute the original object?

It could be possible for an operation to be one way, like moving an integer, but in that case it could just terminate the application, and perhaps if the developer wanted to avoid the one way operation could use a swap method instead.

This would only be possible on default move operators, as if there are any additional logic, it may be difficult for the compiler to do a reverse partial transform.

Am I oversimplifying things? Is there something that I missed which keeps containers from moving objects without a non-throwing move constructor/operator?

JeJo
  • 30,635
  • 6
  • 49
  • 88
Adrian
  • 10,246
  • 4
  • 44
  • 110
  • Doesn't vector fall back on the copy when the move ctor could throw? – JVApen May 29 '18 at 17:45
  • @JVApen, yes it does. I'm wondering if it could be possible to use a move constructor that does throw. – Adrian May 29 '18 at 17:47
  • 2
    Note: not only does the move constructor have to not throw for std containers to move, but it has to be declared `noexcept` – Justin May 29 '18 at 17:47
  • @Justin, yes, I'm aware of that. – Adrian May 29 '18 at 17:47
  • 5
    `vector`'s problem is reverting *completed* moves. You need to move six, moved three, the fourth throws an exception. Even if you require that the fourth isn't changed, you still have no reliable way to revert the first three. – T.C. May 29 '18 at 17:48
  • Don't think so if you guarantee an atomic operation, like vector – JVApen May 29 '18 at 17:48
  • @T.C., good point. That would have to be a special case. – Adrian May 29 '18 at 17:56
  • @T.C., a rotate operation could be used instead, which would avoid this problem, but it would be more expensive. – Adrian May 29 '18 at 18:32
  • Not sure whether this is relevant to what you want(ed) to know, but many containers do not require a (nothrow-)moveable rsp. copyable type because the address of an entry never changes (on certain use cases, see below): `list`, `forward_list`, `deque`, `set`, `map`, `multiset`, `multimap` and the `unordered_` cousins of the latter four. Caveat: You have to use `emplace` operations to truly avoid all moves, and for `deque` this implies no inserting in the middle (which would in fact move other values). Also, no erasing from the middle, only `pop_front()`/`pop_back()`. – Arne Vogel Jun 22 '18 at 08:26
  • 1
    Yes @ArneVogel, I mentioned that [here](https://stackoverflow.com/questions/50590007/could-it-be-possible-to-have-types-with-move-operations-that-throw-in-containers?noredirect=1#comment88191296_50590316). – Adrian Jun 22 '18 at 15:15

2 Answers2

10

You can use types with throwing moves in containers like vector which can move their elements. However, such containers will not use throwing move operations.

Let's say you have a vector of 10 throwing move elements. And the vector needs to resize itself. So it moves 5 objects to the new memory, but the 6th throws. Well, that's OK; construction failed, so the assumption is that the value of the 6th object is fine. That is, whatever that type's exception guarantee is will be how things work.

But then, because the movement of one object failed, vector needs to move the last 5 objects back to the first array, since vector is trying to provide a strong exception guarantee. That's a problem, since the move back can itself fail.

C++ in general does not have valid answers when the process of repairing a failure itself fails. You can see that in exceptions; you can't emit an exception from a destructor that is called during the process of unwinding due to an exception failure. std::terminate happens in this case.

The same goes for vector. If the move back were to fail, vector has no sane answer. As such, if vector cannot guarantee that restoring its previous array state is noexcept, then it will use copying, since that can provide that guarantee.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • One could get around this by using a rotate operation and delete though, right? It would just be a little more expensive. – Adrian May 29 '18 at 18:06
  • @Adrian: Rotate-and-delete what, though? If the type has a throwing-move, how would this "rotate" operation work in a `noexcept` fashion? – Nicol Bolas May 29 '18 at 18:32
  • A rotation operation swaps between two elements at any time. thus, all elements would still be available to recover from. It would be similar to filtering elements in a vector. Items are swapped within the vector and accumulate at the end of it, at which point they are deleted. The only piece that would be missing is some compiler magic to revert the failed move and all preceding ones. Granted, this becomes more complex and requires more time due to that. I'm just thinking out loud. – Adrian May 29 '18 at 18:38
  • 1
    It would be good to note that not all `vector` operations have the strong guarantee. Some of them, such as `erase` and `insert` (except at the end), will move elements around, and if a move throws, no recovery is mandated. Thus throwing moves can be used in those operations. – Howard Hinnant May 29 '18 at 18:47
  • `std::rotate` is not mandated to use `swap`, it can move out to a temporary. If the move back from the temporary throws, you've got a value that can disappear. Additionally, if the general `std::swap` is used, `swap` itself can leave a value stranded in a local variable. So use of `std::rotate` does not address this issue. – Howard Hinnant May 29 '18 at 18:50
  • @HowardHinnant, assuming that some compiler magic is applied, it would be at the level just above the swapping of elements, and would have to somehow keep track of every object it successfully swapped. But I agree, it starts to get quite complicated. – Adrian May 29 '18 at 20:22
  • @Adrian: You cannot apply "compiler magic" to types for which that is not allowed. In order for a type to have a throwing move, it must not be TriviallyCopyable. And therefore, the compiler is *forbidden* from just memcpying their data around. – Nicol Bolas May 29 '18 at 20:37
  • @NicolBolas, I was thinking that it could be new magic for default moveable objects which don't invoke non-trivial copy constructors. Or of it does, then I guess it could just delete the copy. – Adrian May 29 '18 at 23:05
0

First of all, I can hardly imagine object that acquires resources in the move operation. Think about it - unique_ptr just passes the pointer without acquiring anything, same for shared_ptr. string, vector, all the containters etc. just steal the pointers to the resources acquired earlier in default or copy constructor. I feel like throwing from move constructor is like throwing from destructor. Sure, go ahead, shoot yourself in your knee. But ok, I can accept that exceptions from this exist.

So let's move to the second point - when moving there can be a moment when actually both objects (moved from and moved to) are invalid. And to roll back from such situation would require additional 'magic' function to be called to fix one of them. So it seems the data cannot be repaired as standard does not define such a function.

bartop
  • 9,971
  • 1
  • 23
  • 54
  • This question was just conjecture. I was trying to determine if a container be defined for a type that doesn't have a non-throwing move constructor/operator. – Adrian May 29 '18 at 18:10
  • `std::list` and other node-based containers are not required to have `noexcept` move constructors or default constructors. This allows implementations to always have an empty node, which is what happens when you move-from a `std::list`. – Nicol Bolas May 29 '18 at 18:32
  • Yes, @NicolBolas, your right. It is not general to all containers, just vector and similar which allocates in a way such that when inserting/deleting would require a move/copy. – Adrian May 29 '18 at 18:45