Skip to main content

A tale of noexcept swap for user-defined classes in C++11

C++11 has taken another stab at function exception specifications for the masses. The shiny new ‘noexcept’ keyword is phasing out its zombie cousin ‘throw’. The well accepted guideline for the old exception specification is: "don’t use them!" That’s good for us because now we don’t have to bother (the reasons for not using throw specification aren’t for the faint hearted anyways.) The noexcept feature does not appear to be a walk in a park either. So hold on tight!

noexcept is meta-programmable! a.k.a conditional noexcept. It is possible to conditionally specify functions to throw any exceptions. The noexcept specification of functions can be inspected at compile-time and functions can derive their own noexcept specification based on exception specifications found elsewhere in the program. Meta-programs are not off-limits here. An excellent introduction of the noexcept feature and its history can be found in June’11 Overload Journal and on Andrzej's C++ blog. I won’t repeat that here but a quick motivation is in order.

Compile-time knowledge about a move-constructor that it does not throw (i.e., annotated with noexcept(true)) can be used for improved run-time performance. For example, std::vector<T>::resize, std::vector<T>::reserve automatically use T’s move-constructor instead of a copy-constructor if T's move-constructor does not throw. Moving of T objects instead of copying would likely achieve higher performance. The standard library provides std::move_if_noexcept function that does move only if it is guaranteed to not fail. Otherwise, it simply resorts to copy-construction. More on this can be found here. I'll return to the topic of writing noexcept move-ctor towards the end of this post.

But the post is about swapping, so lets talk about that. Here is how the std::pair::swap is declared in C++11.

void pair::swap(pair& p)
  noexcept(noexcept(swap(first, p.first)) &&
           noexcept(swap(second, p.second)));


It basically says that pair's swap will throw if either swapping of first or second member throws. The swapping of first and second possibly resolve to their own namespace-level swap via ADL or else they simply pick up std::swap (because pair::swap is declared in std namespace). This declaration seems to indicate a few things.

  1. For a user-defined type it is now much more preferable to have its own namespace-level swap overload instead of using vanilla std::swap. The namespace-level swap function, being an extension of the type’s interface, is vastly more efficient and often can be written in way that it does not throw. The std::swap function, on the other hand, may throw because it uses a copy-constructor and copy-assignment operator while swapping two objects. If you are like me, your member copy-assignment operator will use copy-and-swap idiom for strong exception safety. That means std::swap will create and destroy 3 copies. What a waste! Moreover, that increases the chance of throwing. Bottom line: If T has a member swap, a swap function should be added in the same namespace as of T.

  2. There are some assumptions buried in the above reasoning: (1) move-constructor is not available for a user-defined T, and (2) it is possible to implement member swap of T that does not throw and you can actually say so.

  3. As far as move-constructor being not available is concerned, I think, there are large number of C++03 libraries, which will acquire move-semantics slowly than you might desire. Till then std::swap will, unfortunately, use copy-ctor and copy-assign. A legacy class with a copy-ctor won’t get an automatic move-ctor because it would do wrong things.

  4. The noexcept specification of the user-defined swap better be accurate. Can we really write a nothrow member swap and say so confidently? Obviously, it depends on the members of the user-defined type that we’re swapping. To me, it’s all uphill from here.


Lets look at how standard library uses noexcept for some of its own classes. We already saw std::pair before. How about std::tuple, which is a generalization of a struct and that’s why may be user-defined classes should follow the same basic principle. Tuple’s member swap is declared as

void tuple::swap(tuple& rhs) noexcept(see below)
// The expression inside noexcept is equivalent to the
// logical AND of the following expressions:

noexcept(swap(declval<T&>(), declval<T&>()))
// where Ti is ith type in the variadic list of types.


Tuple’s member swap inspects the noexcept specifications of its members’ swap functions and derives its own specification from that. That makes sense. If swapping of any of the member throws, the tuple’s swap throws. The declval function belongs to same clique as std::move, std::forward, etc. It is a way to obtain rvalue reference of a type without using a constructor. It suffices to say that declval<T> returns "T&&" and declval<T&> returns "T & &&", which is the same as "T &."

Consider a typical C++03 user-defined legacy class that manages its own resources.

namespace L 
{
struct legacy 
{
  legacy();                             // Does not throw
  legacy(const legacy &);               // This may throw
  legacy & operator = (const legacy &); // This may throw (strong exception safe)
  ~legacy() throw();                    // Does not throw
  void swap(legacy &) throw();          // Does not throw
};
} // namespace L


The above legacy class is well-designed except for the fact that it does not have any namespace-level swap. To make it a better citizen in C++11, it needs a namespace-level swap. Interestingly enough, there is substantial code out there that has a specialization of the std::swap in the std namespace. Considering the popularity of this anti-pattern, it probably makes sense to add a swap in two places. It is not too much work in the first place and that may help people who always use a qualified swap (std::swap) habitually.

namespace L 
{
  void swap(legacy &, legacy &) noexcept;
}

namespace std 
{
  template <>
  void swap(L::legacy &, L::legacy &) noexcept;
}


Let’s try to use this modernized legacy class in our C++11 class: Modern.

namespace M 
{
struct Modern 
{
  int count;
  std::string str;
  std::array<L::legacy, 5> data;
  /// default-ctor, copy-ctor, copy-assign, move-ctor, move-assign are defined = default.
  void swap(Modern & p) 
    noexcept(noexcept(swap(std::declval<int &>(),
                           std::declval<int &>())) &&
             noexcept(swap(std::declval<std::string &>(),
                           std::declval<std::string &>())) &&
             noexcept(swap(std::declval<std::array<L::legacy, 5> &>(),
                           std::declval<std::array<L::legacy, 5> &>())));
};
} // namespace M


That’s one awful looking swap. My eyes bleed when I look at it.

It is doing exactly the same thing as std::tuple. It simply checks whether swapping two integers, two strings, and two arrays throw or not. If it does, then member swap of Modern throws. You can skip checks for integer swapping but what’s left is not pretty at all.

Note: The text in italics below is not accurate anymore as noted in the comment below. C++11 std::swap uses move-construction and move-assignment internally. As a result, std::swap in C++11 is generally very efficient IF the move-operations are defined and they are really more efficient than their copy counterparts. In such cases, there is little need to write a custom namespace-level swap. However, member swap may still be useful. See the "default-construct-and-swap" technique for implementing a move-constructor below.

Couple of important things to note here:
  1. Because we added namespace-level swap for the L::legacy class, we dodged several problems.If we did not have free swap, the compiler will instantiate std::swap if it is in the scope. Note, however, that vanilla std::swap may throw defeating the purpose of Modern::swap. If no swap is to be found, compiler issues an error.

  2. We hope that swap of std::string and std::array do not throw. As mentioned in [container.requirements.general], std::string::swap does not throw but std::array<T> swap may throw if an unqualified call to swap(T &, T &) throws. Once again, the namespace-level swap will be chosen here, if available. If that does not exist, a specialization of std::swap will be chosen. If none of them exist, std::swap will be instantiated giving our Modern::swap a nothrow(false) specification.

I like my strong exception safety in my little copy-assignment operator so I don’t want my swap to throw but I don’t want my program to std::terminate() if an exception is really thrown. With all that in mind, I would rewrite the swap as follows.

void Modern::swap(Modern &) noexcept 
{
  static_assert(noexcept(swap(std::declval<int &>(),
                              std::declval<int &>())) &&
                noexcept(swap(std::declval<std::string &>(),
                              std::declval<std::string &>())) &&
                noexcept(swap(std::declval<std::array<L::legacy, 5> &>(),
                              std::declval<std::array<L::legacy, 5> &>())), 
                "throwing swap");
  //.... remaining swap code
}


This is not unlike what’s proposed by others but there’s more. The static assert looks awful and looks redundant. There is already a standard library utility that does the same thing: std::tuple. As mentioned before, std::tuple’s swap throws if any member swap throws. We use it here to make our job a lot easier.

void Modern::swap(Modern & that) noexcept 
{
  typedef std::tuple<int, std::string, std::array <L::legacy, 5> > MemberTuple;
  static MemberTuple *t = 0;
  static_assert(noexcept(t->swap(*t)), "throwing swap");
  // .... remaining swap code
}


If you an enthusiastically paranoid C++ programmer, you may want to use conditional noexcept with a little meta-function (is_nothrow_swappable_all) to save typing.

template<typename... T>
struct is_nothrow_swappable_all
{
  static std::tuple<T...> *t = 0;
  enum { value = noexcept(t->swap(*t)) };
};

void Modern::swap(Modern & that) 
   noexcept(is_nothrow_swappable_all<int, std::string, std::array<L::legacy, 5> >::value);


The "remaining swap code" section above is also rather important. The recommended (by Dave and Howard) way to write it is to use unqualified swap but bring std::swap in the scope:

void Modern::swap(Modern & that) 
  noexcept(is_nothrow_swappable_all<int, std::string, std::array<L::legacy, 5> >::value);
{
  using std::swap;
  swap(this->count, that->count);
  swap(this->str, that->str);
  swap(this->data, that->data);
}


The appropriate swap (namespace-level via ADL or std::swap) will be chosen depending upon availability. That’s exactly what tuple’s swap noexcept checker is going to do for us.

Back to the move-constructor, as promised! As mentioned before, noexcept specification of a move-ctor may have performance implications. So you want to get it right. For the M::Modern class we could have defaulted the move-ctor (= default). That will give it a noexcept(false) specification automatically because internally it uses L::legacy’s copy constructor, which throws. As a consequence, std::vector::resize is not as fast as it can be. We can do better.

Implementing a move-ctor using the default-construct-and-swap idiom turns out be quite handy. Default-construct-and-swap does what it says by first delegating to a noexcept default constructor followed by a swap. To implement a noexcept move-ctor this way, you really need to make sure that swap does not throw. As always, you can rely on a conditional noexcept. I'm using std::is_nothrow_constructible type trait to test the obvious!

Modern::Modern(Modern && m) 
  noexcept(std::is_nothrow_constructible<Modern>::value &&
           noexcept(m.swap(m)))
  : Modern()           
{
  swap(m);
}


As long as Modern's default constructor and member swap are noexcept, the move-ctor is noexcept.

Finally, the move-assign operator can be simply implemented as a single swap operation but for some classes that may not be accurate.

Modern::operator = (Modern && m) noexcept
{
  swap(m);
}


Acknowledgements: I want to thank Howard Hinnant (former Library Working Group chair) for reviewing this article and providing me valuable feedback. Any inaccuracies and mistakes left in the article are strictly mine.

Comments

Michal Mocny said…
Beautiful article, and thoroughly enjoyable.

Using std::tuple in such a clever way is very creative, and leaves me contemplating other uses..

Thanks!
Anonymous said…
What are the trade offs of using an in-class friend function vs a free function? The namespace search rules for finding in-class friends are slightly different than out-of line functions and seem more amenable to this particular situation. Plus source tools like doxygen do a nicer job of formatting ;)
Anonymous said…
Isn't default contructor + swap inefficient? Couldn't you explicitly std::move the members and zero them out? I feel as if that may use fewer cycles.
Sumant said…
@Anonymous: Not necessarily! Consider the 'legacy' class I talk about in the post. It's a C++03 class that does not have an efficient move constructor. If you attempt to use std::move for an object of such a class, compiler is going to invoke legacy's copy constructor. The key thing to note here is that move construction (construction from an rvalue) does not automatically mean invoking a move constructor. For instance, std::complex move construction will not throw. But this type does not even have a move constructor, much less a non-throwing one. The legacy class in my post provides the same implementation for both: copy construction and move construction. That's a gotcha!

So now when the copy-ctor comes into the picture, things look very different. Copy-ctor is not only inefficient, it may throw. So you get a throwing move-constructor for a user-defined class if you do std::move without looking into what it is really being invoked. default-ctor + swap is always nothrow for the 'legacy' class. So you get a fast, non-throwing move-constructor for the user-defined class.
Anonymous said…
std::swap does not do anything like copy. It uses move semantics. In C++11 there is now very rarely a need to write a custom swap, as std::swap is highly efficient and quite generic.
Anonymous said…
Just curious, instead of this which uses C++98-style traits:

template
struct is_nothrow_swappable_all
{
static std::tuple *t = 0;
enum { value = noexcept(t->swap(*t)) };
};

why not this using C++11 style?

template
bool is_nothrow_swappable_all() {
static std::tuple *t = 0;
return noexcept(t->swap(*t));
}
Sumant said…
@Anonymous: noexcept operator requires that the value of the expression (is_no_throw_swappable_all) be available at compile-time. A regular function won't serve the purpose. A constexpr function probably would.
RocketSunner said…
I discovered that gcc 5.0 does not declare std::string to have a nothrow swap, but Visual Studio 2015 does. There is apparently some controversy about whether string swap and move assign might throw. There is allocator-aware semantics to detect a mismatched allocator and do a copy of the contents instead (which goes for new memory resources and might throw). One would think this could be detected at compile time, in most cases.

It is frustrating because I would like to take the advice of this article and provide nothrow swaps for new classes, with a test at compile time to guarantee they don't throw. But the inconsistency in the treatment between compilers will cause significant problems for portability.

Popular Content

Unit Testing C++ Templates and Mock Injection Using Traits

Unit testing your template code comes up from time to time. (You test your templates, right?) Some templates are easy to test. No others. Sometimes it's not clear how to about injecting mock code into the template code that's under test. I've seen several reasons why code injection becomes challenging. Here I've outlined some examples below with roughly increasing code injection difficulty. Template accepts a type argument and an object of the same type by reference in constructor Template accepts a type argument. Makes a copy of the constructor argument or simply does not take one Template accepts a type argument and instantiates multiple interrelated templates without virtual functions Lets start with the easy ones. Template accepts a type argument and an object of the same type by reference in constructor This one appears straight-forward because the unit test simply instantiates the template under test with a mock type. Some assertion might be tested in

Multi-dimensional arrays in C++11

What new can be said about multi-dimensional arrays in C++? As it turns out, quite a bit! With the advent of C++11, we get new standard library class std::array. We also get new language features, such as template aliases and variadic templates. So I'll talk about interesting ways in which they come together. It all started with a simple question of how to define a multi-dimensional std::array. It is a great example of deceptively simple things. Are the following the two arrays identical except that one is native and the other one is std::array? int native[3][4]; std::array<std::array<int, 3>, 4> arr; No! They are not. In fact, arr is more like an int[4][3]. Note the difference in the array subscripts. The native array is an array of 3 elements where every element is itself an array of 4 integers. 3 rows and 4 columns. If you want a std::array with the same layout, what you really need is: std::array<std::array<int, 4>, 3> arr; That's quite annoying for

Covariance and Contravariance in C++ Standard Library

Covariance and Contravariance are concepts that come up often as you go deeper into generic programming. While designing a language that supports parametric polymorphism (e.g., templates in C++, generics in Java, C#), the language designer has a choice between Invariance, Covariance, and Contravariance when dealing with generic types. C++'s choice is "invariance". Let's look at an example. struct Vehicle {}; struct Car : Vehicle {}; std::vector<Vehicle *> vehicles; std::vector<Car *> cars; vehicles = cars; // Does not compile The above program does not compile because C++ templates are invariant. Of course, each time a C++ template is instantiated, the compiler creates a brand new type that uniquely represents that instantiation. Any other type to the same template creates another unique type that has nothing to do with the earlier one. Any two unrelated user-defined types in C++ can't be assigned to each-other by default. You have to provide a