This thread is about me questioning the efficiency of the copy&swap idiom. Before I can ask this question, however, I will briefly explain what the copy&swap idiom is. For those of you who already know it, you can skip to the next big headline.
The Copy&Swap Idiom
I was reading up on the rule of 3, and found a more elegant way of overriding the assignment operator. The way I initially learned to do it was something like this:
class Foo
{
public:
Foo& operator=( const Foo& cp )
{
if( this == &cp ) return *this;
// copy resources here
return *this;
}
};
HOWEVER, this is not exception safe (for instance, if you are allocating new objects during the copying of resources, and any one of them throws an exception, you'll be looking at a memory leak) and it performs needless checks for self assignment.
The copy&swap idiom solves these problems. It requires your class to have a copy constructor and a swap method in order to work (which is also part of the rule of 3 and a half).
The simplest example of a copy&swap implementation would look like the following:
#include <algorithm> // std::swap
class Foo
public:
Foo( const Foo& cp ) : m_Data(cp.m_Data) /*insert any copyable members into initializer list*/ {}
Foo& operator=( Foo O ) // intentionally not a reference, so copying of the object Foo is forced
{
swap( O );
return *this;
}
void swap( Foo& O )
{
using std::swap;
swap( m_Data, o.m_Data );
/* swap any further copyable members here*/
}
private:
int m_Data;
};
Given this test code:
Foo myObject1;
Foo myObject2;
myObject2 = myObject1; // copy and swap
When calling myObject2 = myObject1, the overridden assignment operator operator=( Object1 ) is called. Now here's the part where the magical thing happens: As seen in the comments in the code, Object1 is not passed by reference, but by value. What does this mean? Two things:
- The copying of Object1 into a temporary is forced, which means the copy constructor of Foo is called before we even enter the overridden assignment method.
- Unlike the "traditional" way of overriding assignment, if an exception is thrown within the copy constructor, it is guaranteed to be handled by the class' destructor as soon as it goes out of scope.
Since std::swap is guaranteed to be exception safe, any further operations are guaranteed to be safe as well.
A far more detailed description can be found here: http://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom
Efficiency
So here's my question. Following the completely unoptimized version of the code, there appear to be a total of 4 copies made of the object. The first copy is done when the copy constructor is first called from when the assignment operator is called. The second, third, and forth copies are done inside std::swap, where it copies each member of the object into a temporary, copies one to the other, and then copies the temporary back before destructing it again.
How efficient is this?
Here is some code for you to play around with using the copy&swap idiom:
#include <iostream>
#include <algorithm>
template <class T>
class Container
{
public:
// default constructor
Container( void );
// copy constructors
Container( const Container<T>& cp );
Container( const Container<T>* cp );
// destructor
~Container( void );
// pushes an element into the container
void push_back( const T& data );
// returns the number of elements currently in the container
const std::size_t& size( void ) const;
// prints the contents of the container to cout
void print( void );
// override subscript operators for easy element access
T& operator[]( const std::size_t& index );
const T& operator[]( const std::size_t& index ) const;
// the copy&swap idiom
Container<T>& operator=( Container<T> o );
void swap( Container<T>& o );
private:
T* m_Data;
std::size_t m_Size;
};
// ---------------------------------------------------------------------
template <class T>
Container<T>::Container( void ) : m_Data( 0 ), m_Size( 0 )
{
}
// ---------------------------------------------------------------------
template <class T>
Container<T>::Container( const Container<T>& cp ) : m_Data( new T[cp.m_Size] ), m_Size( cp.m_Size )
{
for( std::size_t i = 0; i != m_Size; ++i )
m_Data[i] = cp.m_Data[i];
}
// ---------------------------------------------------------------------
template <class T>
Container<T>::Container( const Container<T>* cp ) : m_Data( new T[cp->m_Size] ), m_Size( cp->m_Size )
{
for( std::size_t i = 0; i != m_Size; ++i )
m_Data[i] = cp->m_Data[i];
}
// ---------------------------------------------------------------------
template <class T>
Container<T>::~Container( void )
{
if( m_Data ) delete[] m_Data;
}
// ---------------------------------------------------------------------
template <class T>
void Container<T>::push_back( const T& data )
{
// reallocate memory
T* oldData = m_Data;
m_Data = new T[m_Size+1];
if( oldData )
{
for( std::size_t i = 0; i != m_Size; ++i )
m_Data[i] = oldData[i];
delete[] oldData;
}
// add new data
m_Data[m_Size] = data;
++m_Size;
}
// ---------------------------------------------------------------------
template <class T>
void Container<T>::print( void )
{
for( std::size_t i = 0; i != m_Size; ++i )
std::cout << m_Data[i] << std::endl;
}
// ---------------------------------------------------------------------
template <class T>
const std::size_t& Container<T>::size( void ) const
{
return m_Size;
}
// ---------------------------------------------------------------------
template <class T>
T& Container<T>::operator[]( const std::size_t& index )
{
return m_Data[index];
}
// ---------------------------------------------------------------------
template <class T>
const T& Container<T>::operator[]( const std::size_t& index ) const
{
return m_Data[index];
}
// ---------------------------------------------------------------------
template <class T>
Container<T>& Container<T>::operator=( Container<T> o )
{
swap( o );
return *this;
}
// ---------------------------------------------------------------------
template <class T>
void Container<T>::swap( Container<T>& o )
{
using std::swap;
swap( m_Data, o.m_Data );
swap( m_Size, o.m_Size );
}
// ---------------------------------------------------------------------
// simple test
int main()
{
// test on stack
{
Container<int> test2;
{
Container<int> test;
test.push_back(10);
test.push_back(20);
test.print();
test2 = test; // copy and swap occurs here
}
test2.print();
}
// test on heap
Container<int>* test = new Container<int>();
test->push_back(666);
test->push_back(999);
test->print();
Container<int>* test2 = new Container<int>();
*test2 = *test; // copy and swap occurs here
delete test;
test2->print();
delete test2;
return 0;
}