Deriving from concrete types like std::list

Discussion in 'C++' started by Urs Thuermann, Jul 7, 2011.

  1. 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
     
    Urs Thuermann, Jul 7, 2011
    #1
    1. Advertisements

  2. 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?
     
    Alexander Bartolich, Jul 7, 2011
    #2
    1. Advertisements

  3. Urs Thuermann

    Bo Persson Guest

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

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


    Bo Persson
     
    Bo Persson, Jul 7, 2011
    #3
  4. Urs Thuermann

    Kai-Uwe Bux Guest

    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.

    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
     
    Kai-Uwe Bux, Jul 7, 2011
    #4
  5. Urs Thuermann

    MikeP Guest

    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.
     
    MikeP, Jul 7, 2011
    #5
  6. 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.
    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*>?
     
    Alexander Bartolich, Jul 7, 2011
    #6
  7. Urs Thuermann

    Kai-Uwe Bux Guest

    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.

    You document it - just as the many instances of UB in the standard library.
    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
     
    Kai-Uwe Bux, Jul 8, 2011
    #7
  8. Urs Thuermann

    red floyd Guest

    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.
     
    red floyd, Jul 8, 2011
    #8
  9. Urs Thuermann

    Balog Pal Guest

    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?
     
    Balog Pal, Jul 8, 2011
    #9
  10. * Balog Pal, on 08.07.2011 20:30:
    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
     
    Alf P. Steinbach /Usenet, Jul 8, 2011
    #10
  11. Urs Thuermann

    red floyd Guest

    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.
     
    red floyd, Jul 8, 2011
    #11
  12. Urs Thuermann

    Balog Pal Guest

    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.
    And did you actually use it with all kinds of collections?
     
    Balog Pal, Jul 9, 2011
    #12
    1. Advertisements

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 (here). After that, you can post your question and our members will help you out.