More C++ Idioms/Move Constructor
Move Constructor
[edit | edit source]Intent
[edit | edit source]To transfer the ownership of a resource held by an object to another object in C++03. Note: In C++11, move-constructors are implemented using the built-in rvalue reference feature.
Also Known As
[edit | edit source]- Colvin-Gibbons trick
- Source and Sink Idiom
Motivation
[edit | edit source]Some objects in C++ exhibit so-called move semantics. For example, std::auto_ptr
.
In the code that follows, auto_ptr b
ceases to be useful after the creation of object a
.
std::auto_ptr <int> b (new int (10));
std::auto_ptr <int> a (b);
The copy constructor of auto_ptr
modifies its argument, and hence it does not take a const
reference as a parameter. It poses no problem in the above code because object b
is not a const
object. But it creates a problem when a temporary is involved.
When a function returns an object by value and that returned object is used as an argument to a function (for example to construct another object of the same class), compilers create a temporary of the returned object. These temporaries are short lived and as soon as the statement is executed, the destructor of the temporary is called. The temporary therefore owns its resources for a very short time. The problem is that temporaries are quite like const
objects (it makes little sense to modify a temporary object). Therefore, they can not bind to a non-const reference and as a result, can not be used to call the constructor taking a non-const
reference. A move constructor can be used in such cases.
Solution and Sample Code
[edit | edit source]namespace detail {
template <class T>
struct proxy
{
T *resource_;
};
} // detail
template <class T>
class MovableResource
{
private:
T * resource_;
public:
explicit MovableResource (T * r = 0) : resource_(r) { }
~MovableResource() throw() { delete resource_; } // Assuming std::auto_ptr like behavior.
MovableResource (MovableResource &m) throw () // The "Move constructor" (note non-const parameter)
: resource_ (m.resource_)
{
m.resource_ = 0; // Note that resource in the parameter is moved into *this.
}
MovableResource (detail::proxy<T> p) throw () // The proxy move constructor
: resource_(p.resource_)
{
// Just copying resource pointer is sufficient. No need to NULL it like in the move constructor.
}
MovableResource & operator = (MovableResource &m) throw () // Move-assignment operator (note non-const parameter)
{
// copy and swap idiom. Must release the original resource in the destructor.
MovableResource temp (m); // Resources will be moved here.
temp.swap (*this);
return *this;
}
MovableResource & operator = (detail::proxy<T> p) throw ()
{
// copy and swap idiom. Must release the original resource in the destructor.
MovableResource temp (p);
temp.swap(*this);
return *this;
}
void swap (MovableResource &m) throw ()
{
std::swap (this->resource_, m.resource_);
}
operator detail::proxy<T> () throw () // A helper conversion function. Note that it is non-const
{
detail::proxy<T> p;
p.resource_ = this->resource_;
this->resource_ = 0; // Resource moved to the temporary proxy object.
return p;
}
};
The move constructor/assignment idiom plays an important role in the code snippet shown next. The function func
returns the object by value i.e., a temporary object is returned. Though MovableResource
does not have any copy-constructor, the construction of local variable a
in main
succeeds, while moving the ownership away from the temporary object.
MovableResource<int> func()
{
MovableResource<int> m(new int());
return m;
}
int main()
{
MovableResource<int> a(func()); // Assuming this call is not return value optimized (RVO'ed).
}
The idiom uses a combination of three interesting and standard features of C++.
- A copy-constructor need not take its parameter by
const
reference. The C++ standard provides a definition of a copy-constructor in section 12.8.2. Quoting -- A non-template constructor for class X is a copy constructor if its first parameter is of typeX&
,const X&
,volatile X&
orconst volatile X&
, and either there are no other parameters or else all other parameters have default arguments. -- End quote. - A sequence of conversions via the
detail::proxy<T>
object is identified automatically by the compiler. - Non-const functions can be called on temporary objects. For instance, the conversion function
operator detail::proxy<T> ()
is non-const. This member conversion operator is used to modify the temporary object.
The compiler seeks a copy-constructor to initialize object a
. There is a copy-constructor defined but it accepts its parameter by non-const reference. The non-const reference does not bind to the temporary object return by function func
. Therefore, the compiler looks for other options. It identifies that a constructor that accepts a detail::proxy<T>
object is provided. So it tries to identify a conversion operator that converts the MovableResource
object to detail::proxy<T>
. As a matter of fact, such a conversion operator is also provided (operator detail::proxy<T>()
). Note also that the conversion operator is non-const. That's not a problem because C++ allows invoking non-const functions on temporaries. Upon invocation of this conversion function, the local MovableResource
object (m) loses its resource ownership. Only the proxy<T>
object knows the pointer to T
for a very brief period of time. Subsequently, the converting constructor (the one that takes detail::proxy<T>
as a parameter) successfully obtains the ownership (object a
in main
).
Let's look into the details of how temporary objects are created and used. In fact, the above steps are executed not once but twice in exactly the same way. First to create a temporary MovableResource
object and later to create the final object a
in main
. The second exceptional rule of C++ comes into play when a
is being created from the temporary MovableResource
object. The conversion function (operator detail::proxy<T>()
) is called on the temporary MovableResource
object. Note that it is non-const. A non-const member function can be called on a temporary object unlike real const
objects. Section 3.10.10 in C++ ISO/IEC 14882:1998 standard clearly mentions this exception. More information on this exception can be found here. The conversion operator happens to be a non-const member function. Therefore, the temporary MovableResource
object also loses its ownership as described above. Several temporary proxy<T>
objects also are created and destroyed when the compiler figures out the right sequence of conversion functions. It is possible that the compiler might eliminate certain temporaries using return value optimization (RVO). To observe all the temporaries, use --no-elide-constructors option to the gcc compiler.
This implementation of the move constructor idiom is useful to transfer the ownership of resources (e.g., heap-allocated memory) in and out of a function. A function that accepts MovableResource
by-value serves as a sink function because the ownership is transferred to a function deeper inside the call-stack whereas a function that returns MovableResource
by-value serves as source because the ownership is moved to an outer scope. Hence the name source-and-sink idiom.
Safe Move Constructor
Although the source-and-sink idiom is quite useful at the boundaries of a function call, there are some undesirable side-effects of the above implementation. Particularly, simple expressions that look like copy-assignment and copy-initialization do not make copies at all. For instance, consider the following code:
MovableResource mr1(new int(100));
MovableResource mr2;
mr2 = mr1;
MovableResource
object mr2
looks like it has been copy-assigned from mr1
. However, it silently steals the underlying resource from mr1
leaving it behind as if it is default initialized. The mr1
object no longer owns the heap allocated integer. Such behavior is quite surprising to most programmers. Moreover, generic code written assuming regular copy semantics will likely have very undesirable consequences.
The semantics mentioned above are commonly known as Move Semantics. It is a powerful optimization. However, part of the problem of this optimization, implemented in the above form, is the lack of error messages during compilation of assignment and copy-initialization of expressions that look like a copy but in fact perform a move. Specifically, implicitly copying one MovableResource
from another named MovableResource
(e.g., mr1
) is unsafe and therefore should be indicated by an error.
Note that stealing resources from a temporary object is perfectly acceptable because they do not last long. The optimization is meant to be used on rvalues (temporary objects). Safe move constructor idiom is a way of achieving this distinction without language-level support (rvalue references in C++11).
The safe move constructor idiom prevents implicit move semantics from named objects by declaring private constructor and assignment operator functions.
private:
MovableResource (MovableResource &m) throw ();
MovableResource & operator = (MovableResource &m) throw ();
Key thing to note here is that the parameter types are non-const. They bind to named objects only and not to temporary objects. This little change alone is sufficient to disable implicit moves from named objects but allow moves from temporary objects. The idiom also provides a way to do explicit move from a named object, if desired.
template <class T>
MovableResource<T> move(MovableResource<T> & mr) throw() // Convert explicitly to a non-const reference to rvalue
{
return MovableResource<T>(detail::proxy<T>(mr));
}
MovableResource<int> source()
{
MovableResource<int> local(new int(999));
return move(local);
}
void sink(MovableResource<int> mr)
{
// Do something with mr. mr is deleted automatically at the end.
}
int main(void)
{
MovableResource<int> mr(source()); // OK
MovableResource<int> mr2;
mr2 = mr; // Compiler error
mr2 = move(mr); // OK
sink(mr2); // Compiler error
sink(move(mr2)); // OK
}
The move
function above is also quite idiomatic. It must accept the parameter as a non-const reference to the object so that it binds to l-values only. It turns a non-const MovableResource
lvalue (named object) into an rvalue (temporary object) via conversion to detail::proxy
. Two explicit conversions are necessary inside the move
function to avoid certain language quirks. Ideally, the move
function should be defined in the same namespace as that of MovableResource
class so that the function can be looked up using argument dependent lookup (ADL).
Function source
returns a local MovableResource
object. Because it is a named object, move
must be used to turn it into an rvalue before returning. In main
, first MovableResource
is initialized directly from the temporary returned by func
. Simple assignment to another MovableResource
fails with a compiler error but explicit move works. Similarly, implicit copy to the sink
function does not work. Programmer must explicitly express its interest in moving the object to the sink
function.
Finally, it is also important that the key functions (construction from and to detail::proxy
) be non-throwing to guarantee at least basic exception guarantee. No exceptions should be thrown in the meanwhile, otherwise there will be resource leaks.
C++11 provides rvalue references, which support language-level move semantics, eliminating the need for the Move Constructor idiom.
Historical Alternatives
An old (but inferior) alternative is to use a mutable
member to keep track of ownership to sneak past the const-correctness checks of the compiler. The following code shows how the MovableResource
may be implemented alternatively.
template<class T>
class MovableResource
{
mutable bool owner;
T* px;
public:
explicit MovableResource(T* p=0)
: owner(p), px(p) {}
MovableResource(const MovableResource& r)
: owner(r.owner), px(r.release()) {}
MovableResource & operator = (const MovableResource &r)
{
if ((void*)&r != (void*)this)
{
if (owner)
delete px;
owner = r.owner;
px = r.release();
}
return *this;
}
~MovableResource() { if (owner) delete px; }
T& operator*() const { return *px; }
T* operator->() const { return px; }
T* get() const { return px; }
T* release() const { owner = false; return px; } // mutable 'ownership' changed here.
};
This technique, however, has several disadvantages.
- The copy constructor and copy-assignment operators do not make logical copies. On the contrary, they transfer the ownership from right hand side object to
*this
. This fact is not reflected in the interface of the second implementation ofMovableResource
. The firstMovableResource
class accepts aMovableResource
object by non-const reference, which prevents the use of the object in contexts where a copy is expected. - The Const auto_ptr idiom, which is C++03's way of preventing transfer of ownership is simply not possible with the second implementation of the
MovableResource
class. This idiom depends on the fact thatconst
auto_ptr objects cannot bind to the non-const reference parameters of the move-constructor and move-assignment operators. Making the parametersconst
references defeats the purpose. - The
boolean
flag 'owner' increases the size of the structure. The increase in size is substantial (essentially double) for classes likestd::auto_ptr
, which otherwise contain just a pointer. Doubling of the size is due to compiler-enforced alignment of data.
Known Uses
[edit | edit source]- Boost.Move library
- std::auto_ptr uses "unsafe" version of move constructor.
- The unique_ptr C++03 emulation by Howard Hinnant uses the safe move constructor idiom.