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

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?


Solution 1:

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.

Solution 2:

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

Solution 3:

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?

Solution 4:

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.