Re: inheritance, list of objects, polymorphism
On Dec 16, 12:00 pm, "Alf P. Steinbach" <al...@start.no> wrote:
* Vladimir Jovic:
James Kanze wrote:
General rule: assignment and external copy don't work well with
inheritance. (In my own code, I've gradually been introducing a
PolymorphicObject base class, with a virtual destructor and a
private copy constructor and assignment operator. With the rule
that classes designed to be used polymorphically should inherit
from PolymorphicObject.)
I do not understand why you said that "assignment and external copy
don't work well with inheritance."
Mainly it has to do with C++ variables directly being of the size of
the statically known type and directly containing an object of that
type, instead of just being pointers[1] as in Java and C# and like
languages.
When sizeof(Derived) > sizeof(Base) this means that
Base o = Derived();
performs a /slice/ of the Derived object; 'o' contains only the Base
stuff of that original object.
Not just when the sizes are different. The fact that the derived type
can be bigger than the base type (and that the compiler needs to know
the size static and member variables) may be the motivation here, but
the important point is that an object in C++ (or in Java) cannot
change
its type, and that variables in C++ do have object type (rather than
reference type, as in Java). And slicing occurs even if the sizes are
the same---o has type Base.
In many cases (and almost certainly in his), the base class should be
abstract, which guarantees no slicing (since you can't have instances
of
an abstract type).
Additionally, the copy is now a Base, so any overriding of
functionality in Derived is lost.
Even worse, consider
Derived o;
Base& b = o;
b = Base();
Perhaps Base has a person's name and Derived additional has the
person's birth-year, then the above changes the 'o' name without
updating the birth-year, yielding a Derived instance with inconsistent
information.
For a PolymorphicObject base class like James mentioned you therefore
generally want to introduce two restrictions, and support one main
functionality:
* Inhibit client code "slice" copying.
This is done by making the assignment operator private and the
copy constructor protected. James wrote "private" copy constructor
but that's a bit impractical. For you want to allow derived classes
to be clonable, and cloning is best expressed in terms of internal
copy construction.
Oops. You're right, if you want to support cloning, *and* the base
class has state, you'll need a protected copy constructor. (Again, in
the most common scenario, and the one that should be used here, the
base
class will be an "interface": abstract and without state. And the
derived classes copy constructors can simply ignore it.)
* Make sure that objects can only be created dynamically.
The reasonable way is to make the destructor protected.
In practice, I suspect that this may be overkill if the base class is
abstract. Except for construction, client code will only use the base
class. And there's no way they can accidentally declare a variable
with
the type of the base class.
Also, in the very special case of Expression (and maybe one or two
others), there are scenarios where you don't want dynamic allocation
(despite polymorphism). The mode today is to use templates for
compile
time expression evaluation, but in the past, a virtual hierarchy based
on expression did the trick just as well---provided all of the
instances
of the derived class were temporaries on the stack, so that the
compiler
knew the actual types and could inline the virtual functions. (But as
I
said, that's a very special case.)
* Force use of smart pointer.
James relies on garbage collection so he probably doesn't do
this, but there are two aspects: ensuring that any newly created
object's raw pointer is immediately stored in a smart pointer,
and ensuring that only the smart pointer class has access to
destroy an object.
It depends on context, and I don't use garbage collection everywhere.
(Only when I can.) And as I mentionned somewhere, in this particular
case, boost::shared_ptr is a more than adequate solution. Probably
slower than the obvious alternatives (including garbage collection),
but
probably fast enough, and certainly a lot simpler to implement, unless
you're already using garbage collection.
For this case. In many other cases (entity objects, etc.), you don't
really want smart pointers except temporarily (if then---often,
everything that's necessary to accomplish before the smart pointer
gives
up ownership can be done in the constructor).
One way to do the first it is to overload the
class' allocation function (operator new) so that any direct
'new' expression would be overly complicated. For C++98 then
provide a macro that supplies the requisite magic
incomprehensible expression and ensures the pointer is
immediately wrapped in a smart pointer, before client code can
get at it.
You don't need to be that complicated. Just make the constructors
private, and provide a factory function which returns a smart pointer.
Something like:
class Expression
{
Expression( Expression const& );
Expression& operator=(Expression const& );
protected:
Expression() {}
~Expression() {}
public:
typedef boost::shared_ptr< Expression > Ptr;
virtual double value() const = 0;
};
class AddExpression : public Expression
{
Ptr lhs;
Ptr rhs;
AddExpression( Ptr lhs, Ptr rhs )
: m_lhs( lhs )
, m_rhs( rhs )
{
}
public:
static Ptr create( Ptr lhs, Ptr rhs )
{
return Ptr( new AddExpression( lhs, rhs ) );
}
virtual double value() const
{
return lhs->value() + rhs->value();
}
};
That should go a long way to offering the protection you want.
--
James Kanze