Effective C++ 笔记 —— Item 25: Consider support for a non-throwing swap.
To swap the values of two objects is to give each the other's value. By default, swapping is accomplished via the standard swap algorithm. Its typical implementation is exactly what you’d expect:
namespace std { template<typename T> // typical implementation of std::swap; void swap(T& a, T& b) // swaps a's and b's values { T temp(a); a = b; b = temp; } }
Foremost among such types are those consisting primarily of a pointer to another type that contains the real data. A common manifestation of this design approach is the "pimpl idiom" ("pointer to implementation" — see Item 31). A Widget class employing such a design might look like this:
class WidgetImpl { // class for Widget data; public: // ... // details are unimportant private: int a, b, c; // possibly lots of data — expensive to copy! std::vector<double> v; // ... };
class Widget { // class using the pimpl idiom public: Widget(const Widget& rhs); Widget& operator=(const Widget& rhs) // to copy a Widget, copy its WidgetImpl object. For ... { // details on implementing *pImpl = *(rhs.pImpl); // operator= in general, ... // see Items 10, 11, and 12. } // ... private: WidgetImpl *pImpl; // ptr to object with this };
To swap the value of two Widget objects, all we really need to do is swap their pImpl pointers, but the default swap algorithm has no way to know that. Instead, it would copy not only three Widgets, but also three WidgetImpl objects.
What we’d like to do is tell std::swap that when Widgets are being swapped, the way to perform the swap is to swap their internal pImpl pointers.
In general, we’re not permitted to alter the contents of the std namespace, but we are allowed to totally specialize standard templates (like swap) for types of our own creation.
It’s to have Widget declare a public member function called swap that does the actual swapping, then specialize std::swap to call the member function:
class Widget { // same as above, except for the public: // ... // addition of the swap mem func void swap(Widget& other) { using std::swap; // the need for this declaration is explained later in this Item swap(pImpl, other.pImpl); // to swap Widgets, swap their pImpl pointers } // ... }; namespace std { template<> // revised specialization of void swap<Widget>(Widget& a, Widget& b) // std::swap to swap Widgets, call their { a.swap(b); // swap member function } }
Not only does this compile, it’s also consistent with the STL containers, all of which provide both public swap member functions and versions of std::swap that call these member functions.
Suppose, however, that Widget and WidgetImpl were class templates instead of classes, possibly so we could parameterize the type of the data stored in WidgetImpl:
template<typename T> class WidgetImpl { /*...*/ }; template<typename T> class Widget { /*...*/ };
This is what we want to write:
namespace std { template<typename T> void swap<Widget<T> >(Widget<T>& a, // error! illegal code! Widget<T>& b) { a.swap(b); } }
This looks perfectly reasonable, but it’s not legal. We're trying to partially specialize a function template (std::swap), but though C++ allows partial specialization of class templates, it doesn't allow it for function templates.
When you want to "partially specialize" a function template, the usual approach is to simply add an overload. That would look like this:
namespace std { template<typename T> // an overloading of std::swap void swap(Widget<T>& a, Widget<T>& b) // (note the lack of "<...>" after "swap"), but see below for why this isn't valid code { a.swap(b); } }
In general, overloading function templates is fine, but std is a special namespace, and the rules governing it are special, too. It’s okay to totally specialize templates in std, but it’s not okay to add new templates (or classes or functions or anything else) to std. The contents of std are determined solely by the C++ standardization committee, and we’re prohibited from augmenting what they’ve decided should go there. Alas, the form of the prohibition may dismay you. Programs that cross this line will almost certainly compile and run, but their behavior is undefined. If you want your software to have predictable behavior, you’ll not add new things to std.
So what to do? The answer is simple. We still declare a non-member swap that calls the member swap, we just don’t declare the non-member to be a specialization or overloading of std::swap. For example, if all our Widget-related functionality is in the namespace WidgetStuff, it would look like this:
namespace WidgetStuff { // ... // templatized WidgetImpl, etc. template<typename T> // as before, including the swap member function class Widget { // ... }; template<typename T> // non-member swap function; void swap(Widget<T>& a, Widget<T>& b) // not part of the std namespace { a.swap(b); } }
Now, if any code anywhere calls swap on two Widget objects, the name lookup rules in C++ (specifically the rules known as argument-dependent lookup or Koenig lookup) will find the Widget-specific version in WidgetStuff. Which is exactly what we want.
Suppose you’re writing a function template where you need to swap the values of two objects:
template<typename T> void doSomething(T& obj1, T& obj2) { // ... swap(obj1, obj2); // ... }
What you desire is to call a T-specific version if there is one, but to fall back on the general version in std if there’s not. Here’s how you fulfill your desire:
template<typename T> void doSomething(T& obj1, T& obj2) { using std::swap; // make std::swap available in this function // ... swap(obj1, obj2); // call the best swap for objects of type T // ... }
When compilers see the call to swap, they search for the right swap to invoke. C++'s name lookup rules ensure that this will find any T-specific swap at global scope or in the same namespace as the type T. (For example, if T is Widget in the namespace WidgetStuff, compilers will use argument-dependent lookup to find swap in WidgetStuff.) If no Tspecific swap exists, compilers will use swap in std, thanks to the using declaration that makes std::swap visible in this function. Even then, however, compilers will prefer a T-specific specialization of std::swap over the general template, so if std::swap has been specialized for T, the specialized version will be used.
The one thing you want to be careful of is to not qualify the call, because that will affect how C++ determines the function to invoke. For example, if you were to write the call to swap this way:
std::swap(obj1, obj2); // the wrong way to call swap
you'd force compilers to consider only the swap in std (including any template specializations), thus eliminating the possibility of getting a more appropriate T-specific version defined elsewhere.
At this point, we’ve discussed the default swap, member swaps, nonmember swaps, specializations of std::swap, and calls to swap, so let’s summarize the situation.
First, if the default implementation of swap offers acceptable efficiency for your class or class template, you don't need to do anything. Anybody trying to swap objects of your type will get the default version, and that will work fine.
Second, if the default implementation of swap isn't efficient enough (which almost always means that your class or template is using some variation of the pimpl idiom), do the following:
- Offer a public swap member function that efficiently swaps the value of two objects of your type. For reasons I’ll explain in a moment, this function should never throw an exception.
- Offer a non-member swap in the same namespace as your class or template. Have it call your swap member function.
- If you’re writing a class (not a class template), specialize std::swap for your class. Have it also call your swap member function.
Finally, if you’re calling swap, be sure to include a using declaration to make std::swap visible in your function, then call swap without any namespace qualification.
The only loose end is my admonition to have the member version of swap never throw exceptions.This constraint applies only to the member version! It can’t apply to the non-member version, because the default version of swap is based on copy construction and copy assignment, and, in general, both of those functions are allowed to throw exceptions.When you write a custom version of swap, then, you are typically offering more than just an efficient way to swap values; you’re also offering one that doesn’t throw exceptions. As a general rule, these two swap characteristics go hand in hand, because highly efficient swaps are almost always based on operations on built-in types (such as the pointers underlying the pimpl idiom), and operations on built-in types never throw exceptions.
Things to Remember:
- Provide a swap member function when std::swap would be inefficient for your type. Make sure your swap doesn’t throw exceptions.
- If you offer a member swap, also offer a non-member swap that calls the member. For classes (not templates), specialize std::swap, too.
- When calling swap, employ a using declaration for std::swap, then call swap without namespace qualification.
- It’s fine to totally specialize std templates for user-defined types, but never try to add something completely new to std.