Deriving from concrete types like std::list

U

Urs Thuermann

In the 3rd edition of "The C++ Programming Language", Stroustrup
writes in section 25.2 about concrete types

"Classes such as vector, list [...] are concrete [...]
Also, [...] none are intended as a base for derivation."

and two pages later

"A class that does this well is rarely a good candidate for
the creation of different but related classes through public
derivation."

Some of the following arguments seems valid, but still I don't see why
I shouldn't derive a new class from list.

As a C++ learning project I'm re-implementing a simple interpreter I
wrote in C almost 20 years ago.

I have an abstract base class Stmt from which I derive several
concrete statement types, one of which is the StmtList. I want the
StmtList to inherit from Stmt and from list <Stmt *>, so I can call
push_back() on it in the parser or iterate through it to execute the
statement list. Currently, I have something like this, which works
perfectly and, IMHO, looks quite nice:

class Stmt {
public:
virtual void exec() = 0;
};

class WhileStmt : public Stmt {
Expr *cond;
Stmt *stmt;
public:
virtual void exec() {
while (cond->eval())
stmt->exec();
}
};

class StmtList : public Stmt, public std::list <Stmt *> {
public:
virtual void exec() {
const_iterator it;
for (it = begin(); it != end(); ++it)
(*it)->exec();
}
};

// in the parser
stmt_list->push_back(stmt);

Is this, according to Stroustrup, the wrong approach? Instead of
deriving I could have a member of type list <Stmt *> in class StmtList
but iterating would look more cumbersome and I had to implement my own
push_back() that calls push_back() for that list member, etc. After
all, a statement list *is* a list. Shouldn't I express this
relationship by derivation?

urs
 
A

Alexander Bartolich

Urs said:
[...]
class StmtList : public Stmt, public std::list <Stmt *> {
public:
virtual void exec() {
const_iterator it;
for (it = begin(); it != end(); ++it)
(*it)->exec();
}
};

// in the parser
stmt_list->push_back(stmt);

Is this, according to Stroustrup, the wrong approach? Instead of
deriving I could have a member of type list <Stmt *> in class StmtList
but iterating would look more cumbersome and I had to implement my own
push_back() that calls push_back() for that list member, etc. After
all, a statement list *is* a list. Shouldn't I express this
relationship by derivation?

Oh, my. There are two arguments against this.

First, the nice one: by deriving from std::list you make the interface
of StmtList extremly large. This gives users of StmtList a lot of ways
to mess with its content. Which means that you cannot deduce the behav-
iour of the class from its methods alone. You always have to check that
nobody out there does nothing stupid with its instances. This totally
defeats the reason we are doing OOP.

The second argument is not so nice: your code does not actually work.
That is, your claim that »a statement list *is* a list« is false.
Hint: how many methods of std::list are declared virtual?
 
B

Bo Persson

Urs said:
class StmtList : public Stmt, public std::list <Stmt *> {
public:
virtual void exec() {
const_iterator it;
for (it = begin(); it != end(); ++it)
(*it)->exec();
}
};

// in the parser
stmt_list->push_back(stmt);

Is this, according to Stroustrup, the wrong approach? Instead of
deriving I could have a member of type list <Stmt *> in class
StmtList but iterating would look more cumbersome and I had to
implement my own push_back() that calls push_back() for that list
member, etc. After all, a statement list *is* a list. Shouldn't I
express this relationship by derivation?

Having the list as a member seems a much better idea.

How hard is it to write l.begin() instead of begin()?


Bo Persson
 
K

Kai-Uwe Bux

Alexander said:
Urs said:
[...]
class StmtList : public Stmt, public std::list <Stmt *> {
public:
virtual void exec() {
const_iterator it;
for (it = begin(); it != end(); ++it)
(*it)->exec();
}
};

// in the parser
stmt_list->push_back(stmt);

Is this, according to Stroustrup, the wrong approach? Instead of
deriving I could have a member of type list <Stmt *> in class StmtList
but iterating would look more cumbersome and I had to implement my own
push_back() that calls push_back() for that list member, etc. After
all, a statement list *is* a list. Shouldn't I express this
relationship by derivation?

Oh, my. There are two arguments against this.

First, the nice one: by deriving from std::list you make the interface
of StmtList extremly large. This gives users of StmtList a lot of ways
to mess with its content. Which means that you cannot deduce the behav-
iour of the class from its methods alone. You always have to check that
nobody out there does nothing stupid with its instances. This totally
defeats the reason we are doing OOP.

I do not understand the phrase "deduce the behavior of the class from its
methods alone". What I can see is that the implementation via derivation
from std::list< Stmt* > prevents StmtList from imposing any sort of
restriction on the content of the list of statements that is considered
allowed: client code can append and erase in the list at will. What I fail
to see is why that is wrong.

The second argument is not so nice: your code does not actually work.
That is, your claim that »a statement list *is* a list« is false.
Hint: how many methods of std::list are declared virtual?

No methods in std::list< Stmt* > are virtual. But how would that matter? In
the code shown, there is no attempt at overriding any of them.


Best,

Kai-Uwe Bux
 
M

MikeP

You will find it difficult to convince that your class IS-A list rather
than it HAS-A list. Even more dificult to convince that it
IS-AN-STL-LIST.
 
A

Alexander Bartolich

Kai-Uwe Bux said:
[...]
I do not understand the phrase "deduce the behavior of the class from its
methods alone". What I can see is that the implementation via derivation
from std::list< Stmt* > prevents StmtList from imposing any sort of
restriction on the content of the list of statements that is considered
allowed: client code can append and erase in the list at will. What I fail
to see is why that is wrong.

Oh. That's fundamental. *scratches-head*
Well, let's say that somewhere in your program you have the line

theStmtList->push_back(foo_stmt);

and a few thousand lines away you have

theStmtList->push_back(bar_stmt);

Well, actually you have lot's of those lines. In a lot of different
places. But that's OK, you have it under control. Until one day your
program crashes because a stupid user dared to insert an invalid
statement instance. So you add error checking.

if (theStmtList->isValid(foo_stmt))
theStmtList->push_back(foo_stmt);
else
theStmtList->error("Your foo_stmt stinks.");

Problem is, your program still crashes. Because you forgot to fix
all occurences of that push_back. Fortunately your editor can search
strings across all source files. No problem.

A few months later the program starts to crash again. What happened?
Well, the new team member added a few push_back of his own, but
without error handling. Turns out that the documentation of StmtList
was never updated to include the requirement of calling isValid.

....

A few years later, after lots of changes to validation and error
handling, the documentation is still outdated, and your code is an
inconsistent mess. Some parts use the new error handling, other
parts the old version, and there are also places where the error
handling itself is erroneous. You know you should really have
consolidate this years ago, but now it's too late.
[...]
The second argument is not so nice: your code does not actually work.
That is, your claim that »a statement list *is* a list« is false.
Hint: how many methods of std::list are declared virtual?

No methods in std::list< Stmt* > are virtual. But how would that
matter? In the code shown, there is no attempt at overriding any
of them.

Oh. That's also fundamental. Let's say you have

void foo(std::list<Stmt*>* myList)
{
// ...
delete myList;
}

What happens when you pass an instance of StmtList to foo?
What can you do that programmers working with your code do not fall
into this trap?
What exactly is the advantage of your design, if you cannot actually
use instances of StmtList in place of std::list<Stmt*>?
 
K

Kai-Uwe Bux

Alexander said:
Kai-Uwe Bux said:
[...]
I do not understand the phrase "deduce the behavior of the class from its
methods alone". What I can see is that the implementation via derivation
from std::list< Stmt* > prevents StmtList from imposing any sort of
restriction on the content of the list of statements that is considered
allowed: client code can append and erase in the list at will. What I
fail to see is why that is wrong.

Oh. That's fundamental. *scratches-head*
Well, let's say that somewhere in your program you have the line

theStmtList->push_back(foo_stmt);

and a few thousand lines away you have

theStmtList->push_back(bar_stmt);

Well, actually you have lot's of those lines. In a lot of different
places. But that's OK, you have it under control. Until one day your
program crashes because a stupid user dared to insert an invalid
statement instance. So you add error checking.

if (theStmtList->isValid(foo_stmt))
theStmtList->push_back(foo_stmt);
else
theStmtList->error("Your foo_stmt stinks.");

Problem is, your program still crashes. Because you forgot to fix
all occurences of that push_back. Fortunately your editor can search
strings across all source files. No problem.

A few months later the program starts to crash again. What happened?
Well, the new team member added a few push_back of his own, but
without error handling. Turns out that the documentation of StmtList
was never updated to include the requirement of calling isValid.

...

A few years later, after lots of changes to validation and error
handling, the documentation is still outdated, and your code is an
inconsistent mess. Some parts use the new error handling, other
parts the old version, and there are also places where the error
handling itself is erroneous. You know you should really have
consolidate this years ago, but now it's too late.

You have a point. What you say is that _very_ likely one would want
additional restrictions on the content (which std::list< Stmt* > cannot
enforce). In this particular case, I can sympathise with that: I would
conjecture that null pointers are _not_ allowed in a StmtList. Such a
constraint is not enforced by StmtList as it stands.

[...]
The second argument is not so nice: your code does not actually work.
That is, your claim that »a statement list *is* a list« is false.
Hint: how many methods of std::list are declared virtual?

No methods in std::list< Stmt* > are virtual. But how would that
matter? In the code shown, there is no attempt at overriding any
of them.

Oh. That's also fundamental. Let's say you have

void foo(std::list<Stmt*>* myList)
{
// ...
delete myList;
}

What happens when you pass an instance of StmtList to foo?
UB.

What can you do that programmers working with your code do not fall
into this trap?

You document it - just as the many instances of UB in the standard library.
What exactly is the advantage of your design, if you cannot actually
use instances of StmtList in place of std::list<Stmt*>?

Actually, you can. E.g.:

void clear_list ( std::list< Stmt* > & stmt_list ) { ... }
...
StmtList stmt_list;
...
foo( stmt_list );

What is UB is just deleting a StmtList* through std::list< Stmt* >*. The
claim that, therefore, you cannot use instances of StmtList in place of
std::list< Stmt* > is too strong.

Whether the restriction on deletion is a serious matter depends very much on
local coding guidelines and code base.


Best,

Kai-Uwe Bux
 
R

red floyd

Urs said:
[...]
        class StmtList : public Stmt, public std::list <Stmt *>{
        public:
                virtual void exec() {
                        const_iterator it;
                        for (it = begin(); it!= end(); ++it)
                                (*it)->exec();
                }
        };
        // in the parser
                stmt_list->push_back(stmt);
Is this, according to Stroustrup, the wrong approach?  Instead of
deriving I could have a member of type list <Stmt *> in class StmtList
but iterating would look more cumbersome and I had to implement my own
push_back() that calls push_back() for that list member, etc.  After
all, a statement list *is* a list.  Shouldn't I express this
relationship by derivation?

Oh, my. There are two arguments against this.

First, the nice one: by deriving from std::list you make the interface
of StmtList extremly large. This gives users of StmtList a lot of ways
to mess with its content. Which means that you cannot deduce the behav-
iour of the class from its methods alone. You always have to check that
nobody out there does nothing stupid with its instances. This totally
defeats the reason we are doing OOP.

While many of the STL classes are not intended for *PUBLIC*
derivation,
private derivation seems OK. Note, for example,
std::priority_queue<>,
which has protected members. However, it's destructor is not virtual,
so you probably shouldn't publicly derive.
 
B

Balog Pal

red floyd said:
While many of the STL classes are not intended for *PUBLIC*
derivation, private derivation seems OK.

You don't have virtuals to override. So then what is the benefit of
derivation over just having the thing member rather than base class?
 
A

Alf P. Steinbach /Usenet

* Balog Pal, on 08.07.2011 20:30:
You don't have virtuals to override. So then what is the benefit of derivation
over just having the thing member rather than base class?

If you had read further on in Red Floyd's short article, you'd have seen one reason.

The above seems therefore to be an automatic knee jerk reaction, and given that,
what would be the point of trying to explain this?

No point, I think.


Cheers & hth.,

- Alf
 
R

red floyd

You don't have virtuals to override. So then what is the benefit of
derivation over just having the thing member rather than base class?

How about getting access to those protected members?

And the bit about virtual destructors has been beaten
to death here in c.l.c++. You don't want to risk having
your class destroyed by only the base class destructor.

In my case, for example, I needed a priority queue, with the ability
to delete an arbitrary element within said queue (I was using it for
a timer list). std::priority_queue only allows deletion from the
front of the queue. So I used private derivation to get access to
the protected queue data.
 
B

Balog Pal

red floyd said:
How about getting access to those protected members?

Look, the guideline is pretty simple. You derive if you must. (for access to
virtuals, protected, etc). Otherwise prefer not to derive.

We were talking about STL in general. Which have no virtuals, and protected
members are very rare. I very much doubt I'd want ever to derive from
std::priority_queue. If I need some specific elaborated queue I'd just
write it directly, the way I see fit. Using ONE carefully chosen container
that does the job well. I would not struggle with excess genericity. Or
constraints from STL.
In my case, for example, I needed a priority queue, with the ability
to delete an arbitrary element within said queue (I was using it for
a timer list). std::priority_queue only allows deletion from the
front of the queue. So I used private derivation to get access to
the protected queue data.

And did you actually use it with all kinds of collections?
 

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

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top