Re: inheritance, list of objects, polymorphism
On Dec 16, 5:47 pm, "Alf P. Steinbach" <al...@start.no> wrote:
* James Kanze:
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.
I believe the point you raise was addressed in the immediately
following paragraph:
>> Additionally, the copy is now a Base, so any overriding of
>> functionality in Derived is lost.
Yes, but I found the emphasis on sizeof misleading. The possibly
different sizes may have something to do with the original
motivation---I don't know, but the fact remains that size is
irrelevant
in the modern language.
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).
I wouldn't rely on such a guarantee.
More to the point, you don't yourself rely on such a guarantee. :-)
It depends on context.
Because nothing stops anyone from deriving a concrete class with
further dervied classes.
Well, there's always common sense:-). At some point, you can't
possibly
protect against everything, and if the user is going to ignore the
basic
design of the class completely, he's hosed, and there's not much you
can do about it. In this particular case, from what has been
presented
here, Expression will in fact be an abstract base class, and there
will
only be one (or possibly two, but the intermediate level will also be
abstract) level of derivation.
In other contexts, of course, the situation might be different.
[...]
* 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.
I can think of many ways that someone inadvertently declares an
automatic variable of some concrete derived class.
I think it's better to just design that possible bug vector away.
It's the distinction that you often make in this group between objects
with identity and those with just value, where the former are best
designed so that they can only be used with dynamic allocation.
Interestingly enough, I've never tried to enforce these rules. And
I've
never had problems with clients trying to allocate entity objects
other
than dynamically---by their very nature, they have an open lifetime
which doesn't coincide with any scope. (I have had cases of people
allocating value objects dynamically when they shouldn't. And I do
sometimes wonder if I shouldn't ban dynamic allocation for these. But
then, I'd have to ban it for double and int as well, and I don't know
how to do that. In the end, in such cases, the only solution is
education.)
Anyhow, my choice here is based on my experiences (i.e. the
programmers
I've worked with). Your experiences may be different, and if I did
find
myself in a context where such errors were occuring, I'd take
appropriate steps to prevent them.
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.
That's a per class solution. And it requires one factory function per
constructor. That's sort of ugly, not to mention laborious, and since
you're designing a common PolymorphicObject base class I think you may
save a lot of work by centralizing the functionality there, -- even
though it relies on a convention, that all derived classes also
declare destructors protected (it's a shame that the accessibility
can't be inherited automatically!).
There are IMHO two separate issues. One is declaring instances on the
stack. The other is ensuring that there are no raw pointers to the
object. Essential if you hope to use shared_ptr (which is very
dangerous, and should be avoided in general), and in this case,
possible, since the basic semantics of an expression node are such
that
we can reasonably assume that derived classes will never export their
this pointer. For the second, the only solution is some sort of
factory
function. And again, for the special case of expression nodes, the
factory function can easily be a template (which still requires that
each derived class declare it friend), although I think I'd still
rather
use the boilerplate. (My editor supports copy-paste, and it's not as
if
there's any chance of the code having to be modified.)
--
James Kanze