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?
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.
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.
For more information read the article \"Why shouldn\'t we inherit a class from STL classes?\"
Guideline
A base class must have:
- a public virtual destructor
- or a protected non-virtual destructor
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?
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.
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.
Also, in most cases, you should prefer composition or aggregation over inheritance if possible.
One word: Substitutability
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.