C++/Alternative to Virtual Clone and Slicing
Expert: Ralph McArdell - 12/11/2009
QuestionI've read that the virtual clone method is a solution to the slicing problem. No problem of course with that. But when I first heard of the problem, the first thing that came to mind was to either 1) prevent slicing by making the copy constructor and assignment operator private, -or-, if you need to copy objects 2) make the assignment operator virtual and define it, similar to the virtual clone method, to prevent the slicing.
Please comment.
Thanks.
AnswerWell a clone virtual member function - to use C++ terms - will allow you to create a new copy of the actual type of an object when you only have a reference (C++ pointer or reference type) to its base type.
As you have to call this member function explicitly it will _not_ prevent unintentional slicing as in the following case:
void f( Base b )
{
// ...
}
// ...
Derived d;
f(d);
(assume Derived is a sub-class of Base and that they are copy constructible)
The object d will be sliced to a new copied Base when passed _by value_ to f(), effectively slicing the Derived d object to a Base object.
In the case of 1) in your question - and this is quite a reasonable thing to do for many types in class hierarchies - the compiler will of course whinge if Base is non-copyable, it having private and undefined copy constructor, and presumably similarly for the assignment operator. This technique, with a private and undefined assignment operator, of course also catches:
Base b;
Derived d;
b = d;
Which of course is not helped at all by the clone technique.
As to 2) in your question I presume you wish to do something like:
Derived d;
Derived d2;
Base * pb(&d);
*pb = d2;
And have the assignment operator for Derived be called for pb->operator=( d2 );
Yes this can be made to work BUT the form of the virtual Base::operator= will be:
Base & Base::operator=( Base const & rhs ) { ... } // declared virtual
And this means that _all_ derived classes that override this base assignment operator must provide an assignment operator of a similar form. So rather than using the usual:
Derived & Derived::operator=( Derived const & rhs ) { ... }
You would have to use:
Derived & Derived::operator=( Base const & rhs ) { ... }
[ Note:
The return type can vary from the Base & declared for Base::operator= (as
Derived is derived from Base) using a feature called covariant return types - if,
that is, your compiler supports it. Some, especially older compilers, may not.
If your compiler does not support covariant return types then you would have to
use a Derived::operator= like so:
Base & Derived::operator=( Base const & rhs ) { ... }
-- end note ]
Both forms (and others if needed) can of course be provided as assignment operator functions can be overloaded like other functions.
This means the derived classes' overridden operator= member functions receive a reference to a _base_ object and not a derived object. You therefore need to perform a downcast to obtain a reference to an actual derived object. As there is no way to know that the base object reference you have been handed actually does refer to the required derived type the only safe cast you can perform is a dynamic_cast which is performed at runtime and will throw a std::bad_cast exception if the object referred to is not in fact the expected derived type. If you do not see this consider that the following will all call the Derived operator= (assuming Derived and Derived2 are both publicly derived from Base and all are suitably defined):
Derived d;
Base b;
Base * pb(&d);
*pb = b; // Trying to assign a Base to a Derived
Derived2 d2;
*pb = d2; // Trying to assign a Derived2 to a Derived
So effectively what you loose is compile time type checking and you have to provide this at runtime - you have to ensure you catch and handle all those potential std::bad_cast exceptions.
The other option would be to use static_cast to perform the downcasts but given the ease with which such a scheme can be abused, even if you are careful, it will come back and bite you (or someone else) on the behind - probably badly when you are least expecting it. Assuming an X is a Y is a recipe for weird behaviour and long debugging sessions as memory will seemingly be 'randomly' overwritten, or seemingly spuriously acquire rubbish values etc.
Here is a simple example program having Base, Derived and Derived2 classes and a main that performs some assignments that you can play with:
#include <iostream> // for std::cerr, std::cout etc.
#include <typeinfo> // for std::bad_cast
class Base
{
int state_;
public:
Base() : state_(0) {}
Base( Base const & other ) : state_(other.state_) {}
virtual ~Base() {}
virtual Base & operator=(Base const & rhs);
};
Base & Base::operator=( Base const & rhs )
{
this->state_ = rhs.state_;
return *this;
}
class Derived : public Base
{
int more_state_;
public:
Derived() : more_state_(0) {}
Derived( Derived const & other )
: Base( other )
, more_state_(other.more_state_)
{}
~Derived() {}
Derived & operator=(Derived const & rhs);
Derived & operator=( Base const & rhs );
};
Derived & Derived::operator=( Derived const & rhs )
{
Base::operator=( rhs );
this->more_state_ = rhs.more_state_;
return *this;
}
Derived & Derived::operator=( Base const & rhs )
{
Derived const & d_rhs( dynamic_cast<Derived const &>(rhs) );
Base::operator=( rhs );
this->more_state_ = d_rhs.more_state_;
return *this;
}
class Derived2 : public Base
{
double more_state_;
public:
Derived2() : more_state_(0.0) {}
Derived2( Derived2 const & other )
: Base( other )
, more_state_(other.more_state_)
{}
~Derived2() {}
Derived2 & operator=(Derived2 const & rhs);
Derived2 & operator=( Base const & rhs );
};
Derived2 & Derived2::operator=( Derived2 const & rhs )
{
Base::operator=( rhs );
this->more_state_ = rhs.more_state_;
return *this;
}
Derived2 & Derived2::operator=( Base const & rhs )
{
Derived2 const & d_rhs( dynamic_cast<Derived2 const &>(rhs) );
Base::operator=( rhs );
this->more_state_ = d_rhs.more_state_;
return *this;
}
int main()
{
Derived target;
Base * pTarget(&target);
Derived source_d;
// Call Derived::operator=( Derived const & rhs ) non-virtually
target = source_d;
// Call Derived::operator=( Base const & rhs ) virtually with various RHS types
std::cout << "Assigning Derived source_d to Derived target...";
try
{
*pTarget = source_d;
std::cout << "OK\n";
}
catch ( std::bad_cast & )
{
std::cerr << "FAILED: right hand side is NOT of expected type." << std::endl;
}
Base source_b;
std::cout << "Assigning Base source_b to Derived target...";
try
{
*pTarget = source_b;
std::cout << "OK\n";
}
catch ( std::bad_cast & )
{
std::cerr << "FAILED: right hand side is NOT of expected type." << std::endl;
}
Derived2 source_d2;
std::cout << "Assigning Derived2 source_d2 to Derived target...";
try
{
*pTarget = source_d2;
std::cout << "OK\n";
}
catch ( std::bad_cast & )
{
std::cerr << "FAILED: right hand side is NOT of expected type." << std::endl;
}
}
Have fun.