Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are the consequences of a throwing swap() for a type?

Tags:

c++

c++17

swap

The A swap function must not fail rule of CppCoreGuidelines says:

swap is widely used in ways that are assumed never to fail and programs cannot easily be written to work correctly in the presence of a failing swap. The standard-library containers and algorithms will not work correctly if a swap of an element type fails.

The Make swap noexcept rule of CppCoreGuidelines says:

A swap must not fail. If a swap tries to exit with an exception, it’s a bad design error and the program had better terminate.

Consider a class C, for which a noexcept move constructor, noexcept move assignment operator and noexcept swap() cannot be implemented. Its copy constructor and copy assignment operator are noexcept(false), therefore std::swap<C>() is noexcept(false) too. Should a friend swap() for this class be declared noexcept anyway so as to terminate in case of an exception and avoid the "bad design error"?

The described class C is a rare case. However, C++03 classes, which define a destructor, copy constructor or copy assignment operator, lack noexcept move operations. A custom swap() for such classes may be non-throwing, though not declared noexcept. If they don't have a custom swap(), the std::swap() specialization calls their copy operations, and so can throw. Do such classes commit the "bad design error"?

Can classes with a throwing swap() be used in standard-library containers and algorithms? Does the standard specify that swap() must not throw exceptions or be noexcept(true) for some standard-library functionality to work? If some parts of the standard library do not work correctly when swap() throws, this must be spelled out in the standard somewhere. The standard library often relies on the destructor to be noexcept; falls back on exception-safe copying when the move operations are noexcept(false). I wonder where swap()'s exception specification stands in this regard.

EDIT [for those who cannot imagine a class with a throwing swap()]: my class C (implemented before C++11 by someone else) is externally, optionally reference-counted. A specific object of this class C is reference-counted if a global container contains its address (this). When a reference-counted object is copied or moved into or swapped with a not-reference-counted object (or vice versa), the reference counts have to be changed. Manipulating reference counts is very complicated. Not only does it (optionally) allocate memory, but performs other operations that are more likely to fail than operator new. Wrapping each object into a unique_ptr would cause a huge overhead for the most common not-reference-counted case. Of course, such classes are not commonplace, but I think there are enough of throwing swap()s around to deserve some consideration.

EDIT of the previous EDIT: special member functions must (conditionally) change reference counts because they depend on the object's value. Each object contains a handle to an external resource, which is optionally reference counted. So when the value of a reference-counted object is changed during the copying/moving/swapping with a non-reference-counted object, the reference count of its previous handle has to be decreased and the reference count of its new handle (from the other object) has to be increased.

like image 280
vedg Avatar asked Jan 31 '26 07:01

vedg


1 Answers

Its copy constructor and copy assignment operator are noexcept(false), therefore std::swap<C>() is noexcept(false) too.

This is where you run into a problem.

A type with a throwing copy constructor is a type such that copying the internal state of an object requires performing a throwing operation. If you copy construct a std::vector, the new vector has no state yet. Therefore, it must be able to allocate memory (if the other vector stores stuff), which is potentially throwing.

A move operation requires that the moved-from object be put into some valid state, while absconding with any resources it may possess. As such, a type with a throwing move is one where putting it into a valid-but-unspecified state requires performing a throwing operation.

Many std::list implementations have a head node, which is a node that always exists, which is the node before the first and the node after the last. Some std::list implementations heap-allocate this node. An invariant of such list implementations is that the pointer to the head node cannot be nullptr. Such implementations must, on move, allocate memory for the moved-from object's head node. This is a throwing operation.

Swapping however involves two objects, both of which are currently in a valid state, and afterwards both of which will be in the same valid states that the other one was in. There is nothing about this process that could reasonably require performing a throwing operation.

Oh sure, if you do the 3-copy/move method of swapping, you could get throwing operations. But for any type which has a throwing copy&move, I cannot imagine a circumstance where you cannot implement a non-throwing swap.

And it will undoubtedly be a faster swap implementation than the 3-copy/move method. A 3-copy/move implementation of swap<vector> has to copy 9 pointers, and null out 6 pointers. A direct implementation only has to write to 6 pointers, and null out nothing.

There is no reason to allocate memory on swap, since both objects have all the memory they need; that memory is simply in the wrong object. There is no reason to create other resources (like file handles, etc), since each object owns what the other object wants. Even in the case of std::list, std::swap<std::list> is defined to be noexcept regardless of implementation. It's just copying pointers around. Same for every container and every other standard library type; they are noexcept (in some cases, to the extent that any template arguments are nothrow swappable).

So even in the case of an object with throwing copy&move, swap should be able to be implemented in a non-throwing way.

The danger in making a throwing swap is that basically nobody will expect it. swap is supposed to be a safe operation.

When a reference-counted object is copied or moved into or swapped with a not-reference-counted object (or vice versa), the reference counts have to be changed

Not on swap, they don't.

At the beginning of a swap operation, one of the objects is reference by X number of objects, and the other object has no reference count. At the end of the swap operation, one of the objects is referenced by X number of objects, and the other has no reference count. The number of references is the same after the swap as it is before. Which is which is beside the point: the reference count is the same.

If it's the same at the start as at the end, then there's no reason to modify it when swapping. It may currently be implemented to do so, but it does not have to.


So when the value of a reference-counted object is changed during the copying/moving/swapping with a non-reference-counted object, the reference count of its previous handle has to be decreased and the reference count of its new handle (from the other object) has to be increased.

Let's look at a more concrete example. You have two objects, a and b. a has a pointer to a reference counted object x that has 3 references, and b has a pointer to a non-reference counted object y.

If you swap them, then a should have a pointer to a non-reference counted object y, and b should have a pointer to a reference counted object x with 3 references. That's what swap is supposed to mean: you take the value of the 2 objects and put them into the other one.

If this is what your code does, there is no reason why the reference counts in x and y need to be modified. They are the same at the start as they are at the end. How you achieve this doesn't matter; whatever you're doing to achieve it does not need to be able to throw. You're just shuffling numbers around.

If you are saying that the current "swap" code means that a pointers to y with 3 references, and b points to x with no references, then your "swap` function is a lie.

If this is the case, the consequences of a lying swap are that no standard library function which swaps things will make sense. reverse, sort, and many others will be broken. Not because of throwing, but because your "swap" operation is lying and "swapping" objects doesn't work right.

like image 162
Nicol Bolas Avatar answered Feb 02 '26 22:02

Nicol Bolas