Polymorphism without virtual in C++

F

feel

Hi,All
I am sure it's an old question. But I just find a interesting design
about this: Polymorphism without virtual function in a C++ class.
My solution is for some special case, trust me, very special.
1 single root class tree
2 the leaf(lowest level) classes are sealed which means we should not
inherite class from them.
3 PImpl idiom. There is only one data mumber in root class and there
is no any other data mumber in child class and virtual funtions.

In my solution, the destructor of root class is not virtual, but we
can use base class pointer to point derived class object.

My question is: is this design follow the C++ standard? I tested it in
VS2005. it's ok. How about GCC?I remember that this non-virtual
destructor behavor is undefine in C++ standard.

Here is a simple code example:

class base
{
public:
~base()
{
delete[] p;
};

protected:
int *p;

base():p(new int[10])
{
};

base(int *pp) : p(pp)
{
};
};

class base1 : public base
{
protected:
base1()
{
};
};

class my : public base1
{
public:
my ()
{
p = new int[10];
};
};

int _tmain(int argc, _TCHAR* argv[])
{

base1 *o = new my;

delete o;
return 0;
}
 
J

Joe Greer

Ka-boom! Undefined behaviour.

Not necessarily Ka-boom, you might get nasal demons instead.

Seriously though, the problem is that if you add any member variables to
your derived class, Base' destructor won't know how to clean them up and
will probably only return a fraction of the memory to the heap. The rest
would be left dangling... holding onto file handles, memory, mutexes... IOW
nasal demons. In this very simple case, the undefined behavior may look
like its working, but it wouldn't take much before that changed and it did
something very hard to debug because, of course, the bug would show up
elsewhere as some random behavior.

joe
 
J

Juha Nieminen

feel said:
I am sure it's an old question. But I just find a interesting design
about this: Polymorphism without virtual function in a C++ class.

I really can't see where the polymorphic part of your code is.

You *can* create some kind of polymorphism without using the keyword
'virtual', for example by using member function pointers (rather than
virtual functions), but that doesn't really make too much sense, as
putting member pointers in the class is only counter-productive and
doesn't achieve anything 'virtual' wouldn't.
3 PImpl idiom. There is only one data mumber in root class and there
is no any other data mumber in child class and virtual funtions.

So there's a pointer to dynamically allocated data in your base class.
Exactly which part of this is polymorphic?

(Personally I really can't understand what's all the fuss about the
Pimpl idiom. It only makes classes consume more memory and slower, which
can be especially counterproductive with small classes which have to be
instantiated frequently and in large amounts. The only situation where
there may be an advantage is if the class is large, it's copied around a
lot, and the Pimpl data is reference-counted or CoW'ed.)
In my solution, the destructor of root class is not virtual, but we
can use base class pointer to point derived class object.

The base class destructor can destroy the data in the base class, yes.
I still fail to see the polymorphic part.

The real problem, as presented by others, is that if you ever add
anything in a derived class that needs to be destroyed, deleting the
object through a base-class pointer will most probably not destroy the
data in the derived class.

(Technically speaking deleting through a base class pointer without a
virtual destructor is undefined behavior even if the derived class is
empty, but I suppose most compilers will do what you expect.)
class base
{
public:
~base()
{
delete[] p;
};

protected:
int *p;

base():p(new int[10])
{
};

base(int *pp) : p(pp)
{
};
};

You probably shortened the example for the sake of brevity, but a few
comments nevertheless:

- Allocating unmanaged memory in the initialization list of the
constructor is asking for trouble.

- If objects of this class are ever copied or assigned, problems will
happen.

- Deleting a pointer given to the constructor is dubious practice at
best. You can't know what it's pointing to.
class base1 : public base
{
protected:
base1()
{
};
};

I didn't quite understand what's the purpose of this class. It does
nothing 'base' wouldn't already do.
class my : public base1
{
public:
my ()
{
p = new int[10];
};
};

This is a good example of why member variables in the protected
section are as bad as in the public section. You are assigning something
to 'p' without any regard to what it might have. In this case it has
already memory allocated to it, which is leaked by your assignment.
 
F

feel

My code is a bad example ~_^. But if you can check with AcGe classes
in ObjectARX(from AutoDesk's AutoCAD sdk), you will see the same
design. The special case is:
1 for geometry class, esp for leaf classes, we donot want user to
extend it. if you want to add other kind of geometry type, please add
new class.The leaf of class tree is 'sealed' class.
2 Polymorphism in this design is the Impl pointer in root class. we
can implementation the real version class for every leaf class.
3 Except for leaf class, every class can not be initialize in the
stack.in my example code, you can not do this:
base1 b;

Why? because I donot want a big class in my header file. For geometry
classes,such as AcGeNurbCurve, there are many method we have to put
into interface. but we can split all these method into different
catalog: 3dentity, curve, spline curve. Then you will see a class
tree. There are small number methods in every node(classes in the
tree). And it's meaningfull for a geometry class library. we may do
something for all kind of curves,right? so if we use Pimpl idiom, you
can find Polymorphism.
 
J

James Kanze

@news.datemas.de:
Not necessarily Ka-boom, you might get nasal demons instead.
Seriously though, the problem is that if you add any member
variables to your derived class, Base' destructor won't know
how to clean them up and will probably only return a fraction
of the memory to the heap.

The problem here is that it is undefined behavior, and that
anything can happen. I can't think of a reasonalble
implementation where only part of the memory is freed, but I can
certainly think of cases where it will corrupt the free space
arena, and cause the program to crash, either immediately, or
sometime later.
 
J

James Kanze

feel wrote:
(Personally I really can't understand what's all the fuss
about the Pimpl idiom. It only makes classes consume more
memory and slower, which can be especially counterproductive
with small classes which have to be instantiated frequently
and in large amounts. The only situation where there may be an
advantage is if the class is large, it's copied around a lot,
and the Pimpl data is reference-counted or CoW'ed.)

It reduces coupling, sometimes enormously.
The base class destructor can destroy the data in the base class, yes.
I still fail to see the polymorphic part.
The real problem, as presented by others, is that if you ever add
anything in a derived class that needs to be destroyed, deleting the
object through a base-class pointer will most probably not destroy the
data in the derived class.

The real problem is that even if he never adds anything, it is
undefined behavior.
(Technically speaking deleting through a base class pointer without a
virtual destructor is undefined behavior even if the derived class is
empty, but I suppose most compilers will do what you expect.)

Most don't. (I'd expect an immediate program crash, but with
most, it will work most of the time, only crashing in specific
cases, or much later.)

[...]
class base
{
public:
~base()
{
delete[] p;
};
protected:
int *p;
base():p(new int[10])
{
};
base(int *pp) : p(pp)
{
};
};
You probably shortened the example for the sake of brevity, but a few
comments nevertheless:
- Allocating unmanaged memory in the initialization list of the
constructor is asking for trouble.

Why? In this case, he's doing it in the approved fashion:
passing the pointer immediately to the base class or member.
- If objects of this class are ever copied or assigned, problems will
happen.
- Deleting a pointer given to the constructor is dubious practice at
best. You can't know what it's pointing to.

That depends on the contract. Is boost::shared_ptr dubious
practice? It certainly deletes a pointer given to the
constructor.
 
J

James Kanze

My code is a bad example ~_^. But if you can check with AcGe classes
in ObjectARX(from AutoDesk's AutoCAD sdk), you will see the same
design. The special case is:
1 for geometry class, esp for leaf classes, we donot want user to
extend it. if you want to add other kind of geometry type, please add
new class.The leaf of class tree is 'sealed' class.
2 Polymorphism in this design is the Impl pointer in root class. we
can implementation the real version class for every leaf class.
3 Except for leaf class, every class can not be initialize in the
stack.in my example code, you can not do this:
base1 b;
Why? because I donot want a big class in my header file. For geometry
classes,such as AcGeNurbCurve, there are many method we have to put
into interface. but we can split all these method into different
catalog: 3dentity, curve, spline curve. Then you will see a class
tree. There are small number methods in every node(classes in the
tree). And it's meaningfull for a geometry class library. we may do
something for all kind of curves,right? so if we use Pimpl idiom, you
can find Polymorphism.

This sounds more like letter-envelope than like compilation
firewall. But I'm not sure I've fully understood it. (The
letter-envelope idiom is used to make a class with value
semantics behave polymorphically.)
 
F

feel

This sounds more like letter-envelope than like compilation
firewall.  But I'm not sure I've fully understood it.  (The
letter-envelope idiom is used to make a class with value
semantics behave polymorphically.)

--
James Kanze (GABI Software)             email:[email protected]
Conseils en informatique orientée objet/
                   Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34

Yes, it's just like that. But the difference is:we have to work on
many kind of Envelops.
In classic letter-envelop, we can hide the real data deifinition. but
if we have to work
on a class heirarchy, for example:


Entity
^
|
Curve
^
|
Line

In this case, the real data is in Line, but we have to catalog all the
line's method into Entity, curve.
Then we can work on all kind of entity or curve.In a summary, we have
two requirement:
1 data hide, different data implementation.
2 class heirarchy.

Any comments about these requirements are welcome!
 
J

Juha Nieminen

feel said:
2 Polymorphism in this design is the Impl pointer in root class. we
can implementation the real version class for every leaf class.

Your base class actually looks simply like a (well, some kind of)
smart pointer.

Yet I still fail to see where the polymorphism is, unless your base
class smart pointer is pointing to different class type, which can be
derived and have virtual functions. If it doesn't, then it's simply a
smart pointer which points to some numeric data, and that's it.

I don't believe the definition of "polymorphism" is "each object can
have differing data". If that was the case, then std::string would be
polymorphic because different instances of std::string can have
differing data (eg. different string length and contents).

In fact, I think std::string is a good comparison point. What are the
relevant differences between your classes and std::string (other than
std::string doesn't expose its private data in its protected section)?
Why? because I donot want a big class in my header file.

Is it simply a question of style? You don't want all the private data
of the class to be viewable from the header file?
 
J

James Kanze

On Aug 7, 4:55 pm, James Kanze <[email protected]> wrote:

[...]
Yes, it's just like that. But the difference is:we have to work on
many kind of Envelops.
In classic letter-envelop, we can hide the real data
deifinition. but if we have to work on a class heirarchy, for
example:

In this case, the real data is in Line, but we have to catalog all the
line's method into Entity, curve.
Then we can work on all kind of entity or curve.In a summary, we have
two requirement:
1 data hide, different data implementation.
2 class heirarchy.

I'm not sure I understand. How is this different from the
classical letter-envelope? You declare all of the functions in
Entity, virtual, with an implementation which just forwards to
the letter class.

Or is it that the real "envelope" (which defines the interface)
is Curve, and Entity is just some sort of generic holder
(something like boost::any)? In that case, I'd implement Curve
as a true letter envelope, and provide for some sort of dynamic
casting to get a Curve from Entity, e.g.

class Entity
{
public:
template< typename Envelope >
operator Envelope() {
return Envelope(
dynamic_cast< Envelope& >( *myLetter ).clone() ) ;
}
private:
Entity* myLetter ;
} ;

(Note that this will throw std::bad_cast if Entity isn't really
the requested type.)

So if I have an Entity, and want to use it as a Curve (although
it is really a Line, of course):

Curve c = static_cast< Curve >( entity ) ;

(It's a bit wierd in that you need to use static_cast in order
to get a dynamic_cast. Perhaps a named function would be
preferable to the type conversion operator.)
 
F

feel

On Aug 7, 4:55 pm, James Kanze <[email protected]> wrote:

    [...]




Yes, it's just like that. But the difference is:we have to work on
many kind of Envelops.
In classic letter-envelop, we can hide the real data
deifinition. but if we have to work on a class heirarchy, for
example:
     Entity
       ^
       |
     Curve
       ^
       |
      Line
In this case, the real data is in Line, but we have to catalog all the
line's method into Entity, curve.
Then we can work on all kind of entity or curve.In a summary, we have
two requirement:
1 data hide, different data implementation.
2 class heirarchy.

I'm not sure I understand.  How is this different from the
classical letter-envelope?  You declare all of the functions in
Entity, virtual, with an implementation which just forwards to
the letter class.

Or is it that the real "envelope" (which defines the interface)
is Curve, and Entity is just some sort of generic holder
(something like boost::any)?  In that case, I'd implement Curve
as a true letter envelope, and provide for some sort of dynamic
casting to get a Curve from Entity, e.g.

    class Entity
    {
    public:
        template< typename Envelope >
        operator Envelope() {
            return Envelope(
                dynamic_cast< Envelope& >( *myLetter ).clone() ) ;
        }
    private:
        Entity*         myLetter ;
    } ;

(Note that this will throw std::bad_cast if Entity isn't really
the requested type.)

So if I have an Entity, and want to use it as a Curve (although
it is really a Line, of course):

    Curve c = static_cast< Curve >( entity ) ;

(It's a bit wierd in that you need to use static_cast in order
to get a dynamic_cast.  Perhaps a named function would be
preferable to the type conversion operator.)

--
James Kanze (GABI Software)             email:[email protected]
Conseils en informatique orientée objet/
                   Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34- éšè—被引用文字 -

- 显示引用的文字 -

Thank you for your reply. I think you are right. I do not use virtual
functions as interface
because the size of object. Saying line, we can put two points as data
implementation. But if
put many virtual functinos into class, we have to get a bigger object
than that object should be:
only two points!Object's size is very important in geometry class
design.
Pimpl can make sure the object's size is suitable for this
requirements.
Polymorphism means in this case is like this: we can call curve's
getLength() function to get curve's
length. but for different kind of curves, we have to implementation
different method to compute it.
 
C

Chris M. Thomasson

Thank you for your reply. I think you are right. I do not use virtual
functions as interface
because the size of object. Saying line, we can put two points as data
implementation. But if
put many virtual functinos into class, we have to get a bigger object
than that object should be:
only two points!Object's size is very important in geometry class
design.
Pimpl can make sure the object's size is suitable for this
requirements.
Polymorphism means in this case is like this: we can call curve's
getLength() function to get curve's
length. but for different kind of curves, we have to implementation
different method to compute it.

Some implementations can have a huge vtable, but the size of the object is
going to be a pointer to the vtable:

class Interface {
virtual void func1() = 0;
virtual void func2() = 0;
virtual void func3() = 0;
virtual void func4() = 0;
virtual void func5() = 0;
virtual void func6() = 0;
virtual void func7() = 0;
virtual void func8() = 0;
[on and on...];
virtual ~Interface() = 0;
};

Interface::~Interface() {}


could end of resulting in:


sizeof(Interface) == sizeof(void*);


on some impls...
 
F

feel

Thank you for your reply. I think you are right. I do not use virtual
functions as interface
because the size of object. Saying line, we can put two points as data
implementation. But if
put many virtual functinos into class, we have to get a bigger object
than that object should be:
only two points!Object's size is very important in geometry class
design.
Pimpl can make sure the object's size is suitable for this
requirements.
Polymorphism means in this case is like this: we can call curve's
getLength() function to get curve's
length. but for different kind of curves, we have to implementation
different method to compute it.

Some implementations can have a huge vtable, but the size of the object is
going to be a pointer to the vtable:

class Interface {
virtual void func1() = 0;
virtual void func2() = 0;
virtual void func3() = 0;
virtual void func4() = 0;
virtual void func5() = 0;
virtual void func6() = 0;
virtual void func7() = 0;
virtual void func8() = 0;
[on and on...];
virtual ~Interface() = 0;

};

Interface::~Interface() {}

could end of resulting in:

sizeof(Interface) == sizeof(void*);

on some impls...

En, right.But the size is still a problem. for 1000 lines, we can use
small memory if we do not use virtual functions.
 
J

James Kanze

On 8月8æ—¥, 下åˆ4æ—¶17分, James Kanze <[email protected]> wrote:
Thank you for your reply. I think you are right. I do not use
virtual functions as interface because the size of object.
Saying line, we can put two points as data implementation. But
if put many virtual functinos into class, we have to get a
bigger object than that object should be:

Only the first virtual function adds to the size of the object;
once the object has at least one virtual function, additional
virtual functions are free (at least in all of the
implementations I've every seen or heard of). Increased size is
not a reason to avoid virtual functions.
only two points!Object's size is very important in geometry
class design.
Pimpl can make sure the object's size is suitable for this
requirements.

The compilation firewall idiom actually increases memory use.
Either there's something I don't understand here, or you've
misunderstood something.
Polymorphism means in this case is like this: we can call
curve's getLength() function to get curve's length. but for
different kind of curves, we have to implementation different
method to compute it.

Exactly.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,756
Messages
2,569,534
Members
45,007
Latest member
OrderFitnessKetoCapsules

Latest Threads

Top