Is there any real risk to deriving from the C++ STL containers?
Asked Answered
Z

7

31

The claim that it is a mistake ever to use a standard C++ container as a base class surprises me.

If it is no abuse of the language to declare ...

// Example A
typedef std::vector<double> Rates;
typedef std::vector<double> Charges;

... then what, exactly, is the hazard in declaring ...

// Example B
class Rates : public std::vector<double> { 
    // ...
} ;
class Charges: public std::vector<double> { 
    // ...
} ;

The positive advantages to B include:

  • Enables overloading of functions because f(Rates &) and f(Charges &) are distinct signatures
  • Enables other templates to be specialized, because X<Rates> and X<Charges> are distinct types
  • Forward declaration is trivial
  • Debugger probably tells you whether the object is a Rates or a Charges
  • If, as time goes by, Rates and Charges develop personalities — a Singleton for Rates, an output format for Charges — there is an obvious scope for that functionality to be implemented.

The positive advantages to A include:

  • Don't have to provide trivial implementations of constructors etc
  • The fifteen-year-old pre-standard compiler that's the only thing that will compile your legacy doesn't choke
  • Since specializations are impossible, template X<Rates> and template X<Charges> will use the same code, so no pointless bloat.

Both approaches are superior to using a raw container, because if the implementation changes from vector<double> to vector<float>, there's only one place to change with B and maybe only one place to change with A (it could be more, because someone may have put identical typedef statements in multiple places).

My aim is that this be a specific, answerable question, not a discussion of better or worse practice. Show the worst thing that can happen as a consequence of deriving from a standard container, that would have been prevented by using a typedef instead.

Edit:

Without question, adding a destructor to class Rates or class Charges would be a risk, because std::vector does not declare its destructor as virtual. There is no destructor in the example, and no need for one. Destroying a Rates or Charges object will invoke the base class destructor. There is no need for polymorphism here, either. The challenge is to show something bad happening as a consequence of using derivation instead of a typedef.

Edit:

Consider this use case:

#include <vector>
#include <iostream>

void kill_it(std::vector<double> *victim) { 
    // user code, knows nothing of Rates or Charges

    // invokes non-virtual ~std::vector<double>(), then frees the 
    // memory allocated at address victim
    delete victim ; 

}

typedef std::vector<double> Rates;
class Charges: public std::vector<double> { };

int main(int, char **) {
  std::vector<double> *p1, *p2;
  p1 = new Rates;
  p2 = new Charges;
  // ???  
  kill_it(p2);
  kill_it(p1);
  return 0;
}

Is there any possible error that even an arbitrarily hapless user could introduce in the ??? section which will result in a problem with Charges (the derived class), but not with Rates (the typedef)?

In the Microsoft implementation, vector<T> is itself implemented via inheritance. vector<T,A> is a publicly derived from _Vector_Val<T,A> Should containment be preferred?

Zymogenesis answered 28/5, 2009 at 17:42 Comment(1)
The comment in kill_it is wrong. If the dynamic type of victim is not std::vector, then the delete simply invokes undefined behaviour. The call to kill_it(p2) causes this to happen, and so nothing needs to be added to the //??? section for this to have undefined behaviour.Ibanez
K
25

The standard containers do not have virtual destructors, thus you cannot handle them polymorphically. If you will not, and everyone who uses your code doesn't, it's not "wrong", per se. However, you are better off using composition anyway, for clarity.

Kerseymere answered 28/5, 2009 at 17:53 Comment(1)
When using this practice with STL containers, I've found that private inheritance is a syntactically easier way to express composition.Mcclendon
E
16

Because you need a virtual destructor and the std containers don't have it. The std containers are not designed to act as base class.

Guideline

A base class must have:

  • a public virtual destructor
  • or a protected non-virtual destructor
Edrei answered 28/5, 2009 at 17:53 Comment(9)
That applies when you design for run-time polymorphism. There's nothing wrong with deriving from non-virtual classes if you don't intend to treat them as their base types other then strong coupling.Contortionist
Where in Example B do you see a need for a virtual destructor?Zymogenesis
You can't tell from example B because it's the class declaration and not the use of the class. A user CAN and WILL abuse the class. A base class must have: - a public virtual destructor - or a protected non-virtual destructorEdrei
@Thomas L Holaday: If the classes are declared inside a private section of another class with explicit notes mentioning the problem with virtual destructors then fine. Otherwise sombody somewhere will use them in a way that will requirea virtual destructor and then the code will be impossable to debug and find the real problem.Haslet
@TimW, can you provide an example of a way a user could abuse B that would not be an identical abuse of A?Zymogenesis
@Thomas, easy going: vector<int> *p = getCharges(); delete p; derive private or put the vector as a member. don't derive public.Taunyataupe
@litb, when I step through that code with the debugger, I see no leaks. Do you?Zymogenesis
@Thomas The memory leak is compiler dependent. Your compiler probably didn't add any extra memory for the derived classes but other compliers can. And if somebody else in your team decides that he needs a extra instance variable in the classes, your application will leak.Edrei
@TimW, (1) Adding extra memory for the derived class will cause no leak; the delete operator returns the memory originally allocated, not the sizeof the object at which it is aimed. (2) if someone decides an extra instance variable is required, and the instance variable is a PODS, there will be no leak. If an extra instance variable is needed that requires destruction, then the onus is on the person adding the instance variable to follow the inheritance chain to confirm that the destructor is virtual.Zymogenesis
C
6

One strong counter-argument, in my opinion, is that you are imposing an interface and the implementation onto your types. What happens when you find out that vector memory allocation strategy does not fit your needs? Will you derive from std:deque? What about those 128K lines of code that already use your class? Will everybody need to recompile everything? Will it even compile?

Contortionist answered 28/5, 2009 at 18:10 Comment(2)
How does using inheritance more of an imposition of interface and implementation than using a typedef?Zymogenesis
Well, I didn't compare one to the other - was just saying that public derivation is dangerous in this regard. After all, you can hide privately contained vector behind a pimpl, but you cannot hide your ancestors :)Contortionist
L
5

The issue isn't a philisophical one, it's an implementation issue. The standard containers' destructors aren't virtual, which means there's no way to use runtime polymorphisim on them to get the proper desctructor.

I've found in practice it really isn't that much of a pain to create my own custom list classes with just the methods my code needs defined (and a private member of the "parent" class). In fact, it often leads to better-designed classes.

Lianna answered 28/5, 2009 at 18:2 Comment(0)
O
3

Also, in most cases, you should prefer composition or aggregation over inheritance if possible.

Opener answered 28/5, 2009 at 17:58 Comment(2)
Since strictly speaking it is always possible to simulate inheritance with containment - you just need a lot of static & dynamic casts and a appropriate of conversion operators - does this mean inheritance should be removed from the language?Zymogenesis
@Thomas L Holaday Of course not; perhaps I should clarify. If you aren't going to be specializing the base class but merely need to use the base class features in your class (as in the example) it is better to have a private member of that class in your class. All the other benefits listed in the question are gained simply by having a class not that fact that it inherited from any other particular class.Opener
I
3

Aside from the fact that a base class needs a virtual destructor or a protected non-virtual destructor you are making the following assertion in your design:

Rates, and Charges for that matter, ARE THE SAME AS a vector of doubles in your example above. By your own assertion "...as time goes by, Rates and Charges develop personalities..." then is the assertion that Rates ARE STILL THE SAME AS a vector of doubles at this point? A vector of doubles is not a singleton for example therefore if I use your Rates to declare my vector of doubles for Widgets I may incur some headache from your code. What else about Rates and Charges are subject to change? Are any of the base class changes safely insulated from clients of your design should they change in a fundamental way?

The point is a class is an element, of many in C++, to express design intentions. Saying what you mean and meaning what you say is the reason against using inheritance in this manner.

...Or just posted more succinctly before my response: Substitution.

Iranian answered 28/5, 2009 at 21:48 Comment(0)
G
1

Is there any possible error that even an arbitrarily hapless user could introduce in the ??? section which will result in a problem with Charges (the derived class), but not with Rates (the typedef)?

Firstly, there's Mankarse's excellent point:

The comment in kill_it is wrong. If the dynamic type of victim is not std::vector, then the delete simply invokes undefined behaviour. The call to kill_it(p2) causes this to happen, and so nothing needs to be added to the //??? section for this to have undefined behaviour. – Mankarse Sep 3 '11 at 10:53

Secondly, say they call f(*p1); where f is specialised for std::vector<double>: that vector specialisation won't be found - you may end up matching the template specialisation differently - typically running (slower or otherwise less efficient) generic code, or getting a linker error if an un-specialised version isn't actually defined. Not often a significant concern.

Personally, I consider destruction through a pointer to base to be crossing the line - it may only be a "hypothetical" problem (as far as you can tell) given your current compiler, compiler flags, program, OS version etc. - but it could break at any time for no "good" reason.

If you are confident you can avoid deletion via a base-class pointer, go for it.

That said, a few notes on your assessment:

  • "providing trivial implementations of constructors" - that's a hassle, but one tip for C++03: template <typename A> Classname(const A& a) : Base(a) { } template <typename A, typename B> Classname(const A& a, const B& b) : Base(a, b) { } ... can sometimes be easier than enumerating all the overloads, but doesn't handle non-const parameters, default values, explicit-vs-non-explicit constructors, nor scale to huge numbers of arguments. C++11 provides a better general solution.

Without question, adding a destructor to class Rates or class Charges would be a risk, because std::vector does not declare its destructor as virtual. There is no destructor in the example, and no need for one. Destroying a Rates or Charges object will invoke the base class destructor. There is no need for polymorphism here, either.

  • There is no risk posed by a derived class destructor if the object is not deleted polymorphically; if it is there undefined behaviour whether or not your derived class has a user-defined destructor. That said, you cross from "probably-ok-for-a-cowboy" to "almost-certainly-not-ok" when you add data members or further bases with destructors that perform clean-up (memory deallocation, mutex unlocking, file handle closing etc.)

  • Saying "will invoke the base class destructor" makes it sound like that's done directly with no implicitly-defined derived-class destructor involved or making the call - all an optimisation detail and not specified by the Standard.

Grindle answered 8/10, 2013 at 4:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.