Designofcopyableandmovabletypes
|
This paper demonstrates:
boost::copy()
), which compliments the protocol for explicitly stating in code, where a move is allowed (std::move()
or boost::move()
);CloneablePtr
can thereby be implemented.Frist, a class CopyMoveDemo
is introduced which will help demonstrate how and when various copies and moves occur.
Notice that this class has all the essential member functions: constructors, the destructor, and swap()
. In particular, it has one explicit constructor taking an int
, and one implicit constructor taking a char const*
. The move constructor takes its argument by BOOST_RV_REF(CopyMoveDemo)
which is a macro from Boost.move and expands to CopyMoveDemo&&
in C++11. All the member functions print their shortened names to cout
. The macro call BOOST_MOVABLE2(CopyMoveDemo)
adds CopyMoveDemo& operator=( CopyMoveDemo )
, implemented in terms of pass-by-value and swap [sutt04]#55. This macro isn't part of Boost. It is introduced in this article as an extention to Boost.move. It doesn't make the object movable, but enables move emulation in C++98 mode. In C++11 the macro expands to:
In C++98, in addition to operator=
like above, it enables move emulation by injecting necessary conversion operators, designed by authors of Boost.move.
operator=
is noexcept
. It does not mean that an expression like a = b
doesn't throw. On the contrary, the copy constructor invoked here can throw. But this expression is strongly exception safe: either the assignment succeeds, or a
remains unchanged.Now, to see how it works:
The output of the above program is the same both in C++98 and C++11, and shows how this class works:
CopyMoveDemo() CopyMoveDemo(int) CopyMoveDemo(str) CopyMoveDemo(const&) CopyMoveDemo(&&) ### CopyMoveDemo(const&) swap CopyMoveDemo ~CopyMoveDemo() CopyMoveDemo(int) swap CopyMoveDemo ~CopyMoveDemo() CopyMoveDemo(str) swap CopyMoveDemo ~CopyMoveDemo() CopyMoveDemo(&&) swap CopyMoveDemo ~CopyMoveDemo() ### ~CopyMoveDemo() ~CopyMoveDemo() ~CopyMoveDemo() ~CopyMoveDemo() ~CopyMoveDemo()
The first section of the output demonstrates calls to five different constructors of clsss CopyMoveDemo
. In the middle section of the output you can see how each of the four assignments results in a call to an apropriate constructor followed by a swap operation and a destructor call. Finally, in the third section of the output you can see the effect of the five objects being destroyed.
Now, if instead of using the macro BOOST_MOVABLE2()
, we wanted to use `BOOST_COPYABLE_AND_MOVABLE()`, we would have to implement operator=
in both copy- and move- versions. And since we were going to write a swap() noexcept
anyway [sutt04]#56, BOOST_MOVABLE2()
gives us two exception safe operator=
s for free.
However, sometimes the pass-by-value and swap version of operator=
s may not be the prefered solution. Here are situations, when you may want to avoid the pass-by-value and swap version:
swap() noexcept
; oroperator=
to preserve a part of the state of the assigned-to object (like the capacity of a vector; for more about this see [abra09]); orYou should choose the solution that suits you best. I recommend the pass-by-value and swap idiom by default, because it is simple, safe, and in certain situations might even be more efficient [sutt04]#27.
We'll start examining the subject by designing a class Matrix
:
This class contains a CopyMoveDemo
instead of something like a vector<double>
, so that we can observe when copies and moves occur. If we relay only on the compiler generated: constructors, assignment opearators and the destructor, here's what we get:
C++11 | C++98 |
---|---|
CopyMoveDemo() | CopyMoveDemo() |
CopyMoveDemo(const&) | CopyMoveDemo(const&) |
CopyMoveDemo(&&) | CopyMoveDemo(const&) |
### | ### |
CopyMoveDemo(const&) | CopyMoveDemo(const&) |
swap CopyMoveDemo | swap CopyMoveDemo |
~CopyMoveDemo() | ~CopyMoveDemo() |
CopyMoveDemo(&&) | CopyMoveDemo(const&) |
swap CopyMoveDemo | swap CopyMoveDemo |
~CopyMoveDemo() | ~CopyMoveDemo() |
### | ### |
~CopyMoveDemo() | ~CopyMoveDemo() |
~CopyMoveDemo() | ~CopyMoveDemo() |
~CopyMoveDemo() | ~CopyMoveDemo() |
As you can see, the C++11 version is move-aware, while the C++98 version isn't, but everything compiles and runs correctly in both cases.
CopyMoveDemo
was implemented with the approach described in Boost.move instead, the C++98 version wouldn't compile. The reason is that the copy-assignment operator is autogenerated to take a non-const reference because of how the move emulation is implemented. This is just another reason to prefer the pass-by-value and swap idiom for implementing assignment operators, which we did in class CopyMoveDemo
.We need to add move emulation to class Matrix
. We'll use the macro BOOST_MOVABLE2()
and add the needed members which are:
With this version of class Matrix
the C++98 output becomes the same as in C++11:
There's nothing useful in our Matrix
so far, so let's make Matrix
addable. As recommended in [sutt04]#27, the operator+=
shall do all the work (in our example it will just print "+="), and operator+
will just use +=
. Notice, how the first argument of operator+
is passed by value.
BOOST_MOVE_RETURN_LOCAL(a)
in the implementation of operator+
expands to return a
in C++11 and 'return ::boost::move(a)in C++98. That is because it is recommended not to use
move()when returning a local variable, while the
move()` is required with C++98 move emulation.Let's test the addability of Matrix
:
The output is the same in C++98 and C++11:
CopyMoveDemo() CopyMoveDemo() CopyMoveDemo() #1# CopyMoveDemo(const&) += CopyMoveDemo(&&) ~CopyMoveDemo() #2# CopyMoveDemo(const&) += CopyMoveDemo(&&) += CopyMoveDemo(&&) ~CopyMoveDemo() ~CopyMoveDemo() #3# CopyMoveDemo(const&) += CopyMoveDemo(&&) CopyMoveDemo(const&) += CopyMoveDemo(&&) ~CopyMoveDemo() ~CopyMoveDemo() ~CopyMoveDemo() ### ~CopyMoveDemo() ~CopyMoveDemo() ~CopyMoveDemo() ~CopyMoveDemo() ~CopyMoveDemo() ~CopyMoveDemo()
As you can see, Matrix
is now addable, but:
+
results in at least one copy of a Matrix
which is eventually moved from;operator+
;#3#
two temporary copies are actually created; we could avoid one of them by reordering the operations or adding some overloads of operator+
, but the compiler doesn't even warn about the additional copy.I think the problem here is not that the copies are made. The problem is that the copies are made silently. Just like silent move in std::auto_ptr
is considered evil, I believe silent copies are a bad, as they are a potential source of pessimization.
#3#
can be avoided in C++11 by providing four overloads of operator+
(yes, you need all four overloads so that all use cases are unabiguous), but that doesn't fix the root of the problem, which I believe is that the copies are silent. operator+
: The conclusion is, the copy constructor should be explicit
.
In C++11 making the copy constructor explicit
is exactly what we can do:
However, in C++98 making the copy constructor explicit
would ruine the move emulation and Matrix
would become silently movable (like std::auto_ptr
) which is unacceptable. But we can make Matrix
noncopyable, and then provide some other way (not via the copy constructor) to make a copy of a Matrix
.
Consider, how usage of Matrix
would change, if it was noncopyable. All we need to change in class Matrix
to make it noncopyable is:
And now compiling the last main()
function results in expected compile-time errors, as comments indicate below:
—TODO Matrix
is noncopyable so far, so we can't make copies, but we can still get this to compile by indicating that certain objects may be moved-from. Not all need to be moveable: only the first argument to operator+
. Also in #2#
it's enough to mark the first argument with boost::move()
, because the result of the first call to +
is a temporary which is ready to be moved by definition. To avoid the second boost::move()
in #3#
we can reorder the operations (after all, +
is commutative; we could also just remove the parentheses, because +
is also associative).
Whenever one wants to allow a move of a non-temporary object, one uses boost::move()
(or std::move()
) just like in the main function above. And so a similar function for explicitly allowing a copy is proposed: boost::copy()
. It is to be used like boost::move()
, whenever one wants to make a copy of a non-temporary object, so that the copy can be moved from, and the original remains unmodified.
So suppose we don't want to allow any moving in the last example and we want copies instead. Here's what we need to say: just replace boost::move()
with boost::copy()
.
For this to work we also need to change one line in class Matrix
:
That's it. Now the above sample compiles and works both in C++11 and C++98, making only the three explicitly requested copies. The following output is from running the C++11 version, while in C++98 more temporary objects are created, but they are only used for moving the existing copies of Matrix
around, and no additional copies are introduced. That is because in C++11 some temporaries can be optimized away by move-ellision, while it's not allowed with move emulation in C++98.
CopyMoveDemo() CopyMoveDemo() CopyMoveDemo() #1# CopyMoveDemo(const&) += CopyMoveDemo(&&) ~CopyMoveDemo() #2# CopyMoveDemo(const&) += CopyMoveDemo(&&) += CopyMoveDemo(&&) ~CopyMoveDemo() ~CopyMoveDemo() #3# CopyMoveDemo(const&) += CopyMoveDemo(&&) += CopyMoveDemo(&&) ~CopyMoveDemo() ~CopyMoveDemo() ### ~CopyMoveDemo() ~CopyMoveDemo() ~CopyMoveDemo() ~CopyMoveDemo() ~CopyMoveDemo() ~CopyMoveDemo()
The tools presented in this paper are part of an extension to Boost.move. That is, they use Boost.move and operate with it. They are:
boost::copy()
– a protocol for stating explicitly, where a copy is to be made;BOOST_MOVABLE2()
– for creating movable types, taking advantage of the pass-by-value and swap idiom (which would be impossible with BOOST_COPYABLE_AND_MOVABLE()
macro); together with BOOST_MOVABLE2()
you optionally may use:BOOST_DELETED_COPY_CONSTRUCTOR()
, orBOOST_EXPLICIT_COPY_CONSTRUCTOR()
, orYou can customize boost::copy()
for any type, even if it is not copyable at all, and make it explicitly copyable. You only need to overload the function copy_adl()
for your type in your type's namespace, and boost::copy()
will find it via the ADL. For example, it's easy to create a CloneablePtr
out of unique_ptr
with a custom deleter – behold:
boost::heap_clone_allocator
from Boost.ptr_container: make_*
function to make it a user-friendly smart pointer: copy_adl()
: Simple and powerful.
In conclusion, in this paper the pass-by-value and swap idiom for implementing assignment operators is recommended, as well as using an extension to Boost.move which uses this idiom. Copyable and movable types are recommended to be explicitly copyable and boost::copy()
is proposed as the protocol for explicitly requesting a copy in code. By customizing boost::copy()
, any not necessarily copyable type can be made explicitly copyable via boost::copy()
.
The Boost.move library
By Ion Gaztanaga
The Boost.ptr_container library
By Thorsten Ottosen
C++ Coding Standards: 101 Rules, Guidelines, and Best Practices
By Herb Sutter, Andrei Alexandrescu
Posted September 28, 2009 by Dave Abrahams, under Value Semantics