Polymorphism & Assignment Op. & Copy Constructors in C++
-
The following question is about how to handle assignment operators (& copy constructors) in a hierarchy of polymorphic objects in order to be able to work with all kind of assignments & copy: i.e. we would ideally like to work always with base class pointers, but also allow to use directly derived class pointers. In fact I do present two techniques to deal with this argument: A) The first does make use of "virtual" assignment operators and, as far as I know, should be the "standard" way to deal with this argument in C++, but it has the drawback that as soon as your hierarchy is more than one level deep it leads to a combinatorial explosion of assignment operators. This technique I call "Polymorphism A" in the following. B) The second technique does make use of an aux virtual copy method, using this aux virtual copy method fewer assignment operators have to be defined, but it has the drawback that you cannot call directly the base class assignment op. from derived ones. This technique I call "Polymorphism B" in the following. My question is more like an open discussion: i.e. I do ask what are your opinions on both techniques, what drawbacks you may see in them, and, eventually, if you do know of a better way to solve the same problem. In the sample code I give objects are relatively simple, but keep in mind that we would like to apply those techniques to complicated objects where "deep" copies are to be used. Let us see the code: ------- Polymorphism A - hierarchy --------------------
// PolymorphismA.cpp
#include "stdafx.h"
#include #include #include #include #include #include using namespace std;
// We want, inasmuch as possible, to work only with CBase pointers
// but we have a hierarchy based on CBase derived classesclass CBase // CBase is our base class
{
private:
CBase() // default ctor is protected, we do not want to default construct
{ cout << "CBase() default ctor called" << endl;
_data = -1; _isvalid = false; };public:
explicit CBase(const int data) // explicit Construction
{ cout << "CBase(const int data) explicit ctor called" << endl;
_data = data; _isvalid = true; };
virtual ~CBase() // polymorphism, virtual dtor
{ cout << "~CBase() dtor called" << endl; };public:
// CBase and derived objects are copy-constructible and assignable through base pointers
CBase(const CBase& iOther) // it cannot be declared as virtual -
The following question is about how to handle assignment operators (& copy constructors) in a hierarchy of polymorphic objects in order to be able to work with all kind of assignments & copy: i.e. we would ideally like to work always with base class pointers, but also allow to use directly derived class pointers. In fact I do present two techniques to deal with this argument: A) The first does make use of "virtual" assignment operators and, as far as I know, should be the "standard" way to deal with this argument in C++, but it has the drawback that as soon as your hierarchy is more than one level deep it leads to a combinatorial explosion of assignment operators. This technique I call "Polymorphism A" in the following. B) The second technique does make use of an aux virtual copy method, using this aux virtual copy method fewer assignment operators have to be defined, but it has the drawback that you cannot call directly the base class assignment op. from derived ones. This technique I call "Polymorphism B" in the following. My question is more like an open discussion: i.e. I do ask what are your opinions on both techniques, what drawbacks you may see in them, and, eventually, if you do know of a better way to solve the same problem. In the sample code I give objects are relatively simple, but keep in mind that we would like to apply those techniques to complicated objects where "deep" copies are to be used. Let us see the code: ------- Polymorphism A - hierarchy --------------------
// PolymorphismA.cpp
#include "stdafx.h"
#include #include #include #include #include #include using namespace std;
// We want, inasmuch as possible, to work only with CBase pointers
// but we have a hierarchy based on CBase derived classesclass CBase // CBase is our base class
{
private:
CBase() // default ctor is protected, we do not want to default construct
{ cout << "CBase() default ctor called" << endl;
_data = -1; _isvalid = false; };public:
explicit CBase(const int data) // explicit Construction
{ cout << "CBase(const int data) explicit ctor called" << endl;
_data = data; _isvalid = true; };
virtual ~CBase() // polymorphism, virtual dtor
{ cout << "~CBase() dtor called" << endl; };public:
// CBase and derived objects are copy-constructible and assignable through base pointers
CBase(const CBase& iOther) // it cannot be declared as virtualThat looks like a shed load of code just to implement copying and assignment. Are you sure that copying objects of arbitrary types is both necessary to your design and won't turn into a complete millstone around your development team's neck? They might not thank you when they have to grind that lot out again to implement a new class? I'm also wincing at how the assignment operators are written - the canonical way you write an assignment operator in C++ is to use a copy and swap. So... What would I suggest? Firstly go back to your design and see if it can be reworked to use a set of relatively simple principles: - value types have no polymorphic behaviour and are copy constructable and assignable if appropriate. Use copy and swap to write the assignment operator for a value type - polymorphic types are accessed through interfaces. Don't assign or copy the objects themselves, reference them through pointers or references to the interfaces they implement. The only place that knows the concrete type of a polymorphic type is the lump of code that creates the object - don't use implementation inheritance. Containment gives far less headaches - minimise manual memory management wherever possible. Use parameterise from above (PFA) and objects created on the stack wherever possible. If you write a delete statement pinch yourself and look at using std::unique_ptr or std::shared_ptr instead. RAII is your friend - make everything exception safe but don't handle exceptions in too many places. One try/catch set in a function If there's still a need to deep copy objects through pointers to arbitrary base classes then consider implementing a clonable interface. Never slice objects - once you lop off the context you can never get it back.
-
That looks like a shed load of code just to implement copying and assignment. Are you sure that copying objects of arbitrary types is both necessary to your design and won't turn into a complete millstone around your development team's neck? They might not thank you when they have to grind that lot out again to implement a new class? I'm also wincing at how the assignment operators are written - the canonical way you write an assignment operator in C++ is to use a copy and swap. So... What would I suggest? Firstly go back to your design and see if it can be reworked to use a set of relatively simple principles: - value types have no polymorphic behaviour and are copy constructable and assignable if appropriate. Use copy and swap to write the assignment operator for a value type - polymorphic types are accessed through interfaces. Don't assign or copy the objects themselves, reference them through pointers or references to the interfaces they implement. The only place that knows the concrete type of a polymorphic type is the lump of code that creates the object - don't use implementation inheritance. Containment gives far less headaches - minimise manual memory management wherever possible. Use parameterise from above (PFA) and objects created on the stack wherever possible. If you write a delete statement pinch yourself and look at using std::unique_ptr or std::shared_ptr instead. RAII is your friend - make everything exception safe but don't handle exceptions in too many places. One try/catch set in a function If there's still a need to deep copy objects through pointers to arbitrary base classes then consider implementing a clonable interface. Never slice objects - once you lop off the context you can never get it back.
Thanks Aescleal, all the techniques you suggest are fine, but my point was more on the discussion of the copy ctor and assignment op. themselves: it is clear that if I make them private, or if I just use the compiler generated ones for POD types, there will be no worry. By the way what do you mean exactly by "copy and swap" ? Do you mean this ? :
Object& operator = (const Object& other)
{
Object(other).swap(*this);
return *this;
}(How do you ensure no self assignment in this case ?, by forwarding to std::swap ?) My point anyway was about how to do in the correct way when I've a hierarchy, I've deep copies, I've to be able to use base class pointers or derived pointers: i.e. in the most general case. In this general case it seems to me that you're forced to use one of the given approaches, and that you shall handle all cases of copy ctor and assignment ops explicitly (derived from derived, derived from base, base from derived, base from base) maybe just disallowing some of them. Just to be clear, my point is the following:
CBase* pbase0 = new CDerived(...);
CBase* pbase1 = new CDerived(...);*pbase0 = *pbase1 // Will it works correctly or will it slice and I should disallow it ?
Bye Federico
-
Thanks Aescleal, all the techniques you suggest are fine, but my point was more on the discussion of the copy ctor and assignment op. themselves: it is clear that if I make them private, or if I just use the compiler generated ones for POD types, there will be no worry. By the way what do you mean exactly by "copy and swap" ? Do you mean this ? :
Object& operator = (const Object& other)
{
Object(other).swap(*this);
return *this;
}(How do you ensure no self assignment in this case ?, by forwarding to std::swap ?) My point anyway was about how to do in the correct way when I've a hierarchy, I've deep copies, I've to be able to use base class pointers or derived pointers: i.e. in the most general case. In this general case it seems to me that you're forced to use one of the given approaches, and that you shall handle all cases of copy ctor and assignment ops explicitly (derived from derived, derived from base, base from derived, base from base) maybe just disallowing some of them. Just to be clear, my point is the following:
CBase* pbase0 = new CDerived(...);
CBase* pbase1 = new CDerived(...);*pbase0 = *pbase1 // Will it works correctly or will it slice and I should disallow it ?
Bye Federico
Generally: Deep hierarchies (i.e. anything more than inheriting an interface) are bad news. Implementation inheritance is the strongest coupling there is and you'll end up with fragile logical designs (you'll spend ages trying to work out what goes where in your class taxonomy and find you have to shuffle a lot of members around) and long build times (when you shuffle you'll have to rebuild the world). If you stick to parameterise from above and interface inheritance you won't have to copy objects, just references and the whole question becomes moot. Copy and swap is:
T &T::operator=( const T &t )
{
T temp( t );
swap( temp );
return *this;
}where swap is non-throwing. You get exception safety for free and if you use the PIMPL idiom and lean on std::unique_ptr it's pretty trivial to implement.
-
Generally: Deep hierarchies (i.e. anything more than inheriting an interface) are bad news. Implementation inheritance is the strongest coupling there is and you'll end up with fragile logical designs (you'll spend ages trying to work out what goes where in your class taxonomy and find you have to shuffle a lot of members around) and long build times (when you shuffle you'll have to rebuild the world). If you stick to parameterise from above and interface inheritance you won't have to copy objects, just references and the whole question becomes moot. Copy and swap is:
T &T::operator=( const T &t )
{
T temp( t );
swap( temp );
return *this;
}where swap is non-throwing. You get exception safety for free and if you use the PIMPL idiom and lean on std::unique_ptr it's pretty trivial to implement.
Hello Aesclal, thanks again, may I ask you (if you got the time) to append some links in order to let other readers as well understand the topic ? (e.g. spelling out what is PFA - parameterise from above - and its coupling with interface inheritance, why you will then work only with references, etc...). The best would be a link to a public domain example... About "swap", I left it out first from my discussion in order to focus only on copy ctor & assignment operators, otherwise you should add also the swap method to the examples. Anyway, usually I would add this to the assignment op:
T &T::operator=( const T &t )
{
if ( this != &t ) // self assignment, you may also think of using address-of to be safer
{
T temp( t );
swap( temp );
}
return *this;
}The PIMPL idiom (for the sake of other readers) is the private implementation idiom. Bye Federico