5

I have repeatedly read that inheriting from STL containers is a Bad Thing.
(I know what the reasons behind this are, that's not my question.)

Keeping the above in mind, what is the proper way to extend the functionality of a container?

For example, if I want a vector-like container that automatically resizes when the argument of operator[] is greater than or equal to the size of the container, what would I do?

To me, the most obvious solution in C++11 is:

template<class T, class A = std::allocator<T> >
class auto_vector : public std::vector<T, A>
{
    typedef std::vector<T, A> base_type;
public:
    using base_type::vector;
    typename base_type::reference operator[](typename base_type::size_type n)
    // I don't need const version, so I'll just ignore it here
    {
        if (n >= this->base_type::size())
        { this->base_type::resize(n + 1); }
        return this->base_type::operator[](n);
    }
};

In C++03, it would be:

template<class T, class A = std::allocator<T> >
class auto_vector : public std::vector<T, A>
{

    typedef std::vector<T, A> base_type;
public:
    explicit auto_vector(
        typename base_type::allocator_type const &alloc =
            typename base_type::allocator_type())
        : base_type(alloc) { }
    explicit auto_vector(
        typename base_type::size_type n,
        value_type const &val = value_type(),
        typename base_type::allocator_type const &alloc =
            typename base_type::allocator_type())
        : base_type(n, val, alloc) { }
    template<class InIt>
    auto_vector(InIt f, InIt l, typename base_type::allocator_type const &alloc =
                                typename base_type::allocator_type())
        : base_type(f, l, alloc) { }
    auto_vector(auto_vector const &v) : base_type(v) { }
    typename base_type::reference operator[](typename base_type::size_type n)
    // I don't need const version
    {
        if (n >= this->base_type::size())
        { this->base_type::resize(n + 1); }
        return this->base_type::operator[](n);
    }
};

Both of these are bad practice because they inherit from std::vector.

What, then, is the "proper" way to implement something like this?

Community
  • 1
  • 1
user541686
  • 205,094
  • 128
  • 528
  • 886
  • I don't see what the huge deal with inheriting from them is if you don't add any members, but that's just me. In addition standard container adaptors (which vector is not) have protected members which is just asking to be inherited from – aaronman Dec 14 '13 at 03:54
  • At the start of the cited reference, "you should have a virtual destructor in a class if you want to make it a base class" is just nonsense. Also known as balderdash, lunacy, mumbo-jumbo, etc. Any conclusions derived from that nonsense are necessarily invalid. Might be wrong or right, but invalid as conclusions. – Cheers and hth. - Alf Dec 14 '13 at 03:54
  • @Cheersandhth.-Alf: Destructor should be nonpublic if nonvirtual, otherwise it can be public... I think they forgot the other case but I'm pretty sure this is commonly stated as best practice. – user541686 Dec 14 '13 at 03:56
  • @aaronman: The issue is that they're susceptible to being deleted via a base class pointer, since the destructors are public. Google "inherit from STL container" to see what I mean, everyone advises against it... – user541686 Dec 14 '13 at 03:57
  • @Mehrdad: is that so. what's your opinion on the design of e.g. `std::ofstream`? – Cheers and hth. - Alf Dec 14 '13 at 03:57
  • @Mehrdad but the destructor will work fine if you don't add members, or virtual functions I think – aaronman Dec 14 '13 at 03:58
  • @Cheersandhth.-Alf: `ofstream`? Doesn't it have a virtual destructor? Edit: I think my comment above was misleading, corrected... – user541686 Dec 14 '13 at 03:59
  • @aaronman: I don't think in general you can delete a derived-class object via a base-class pointer if the destructor is not virtual, even if the derived class doesn't "add" anything... what makes you think otherwise? – user541686 Dec 14 '13 at 04:04
  • @Mehrdad I always wondered about that, I can't see why it would be an issue to delete if it has exactly the same members, I'll take a look in the standard – aaronman Dec 14 '13 at 04:05
  • @aaronman: Let me know if you find anything, now you've made me curious! Maybe post it as another question if you don't see it. – user541686 Dec 14 '13 at 04:06
  • 1
    @Mehrdad: yes. but let's consider `std::stack`. it's a class with a `protected` member, which can only be accessed in derived classes. so it's designed for derivation. yet it does *not* have a virtual destructor. that's existing practice, right in the standard library for you, demonstrating that the article you cited is nonsense. – Cheers and hth. - Alf Dec 14 '13 at 04:08
  • @Cheersandhth.-Alf: Good point. You should post that as an answer, it's a perfectly good answer for me. I also suggest you post it [here](http://stackoverflow.com/questions/2034916), [here](http://stackoverflow.com/questions/4353203), and elsewhere because it's a really good counterexample to everyone's advice on this matter on StackOverflow. – user541686 Dec 14 '13 at 04:10
  • @Cheersandhth.-Alf totally agree, people always advise against it but why on earth would the standard give container adaptors protected members if they weren't meant to be inherited from – aaronman Dec 14 '13 at 04:14
  • @Mehrdad: Thank you for those references, but no, I'm not going to fight the prevalence of Herb Schildt-info on SO. First, there's too much of it even for an army of engineers. Secondly, I would be (yet again!) brushing or even bruising a lot of egos by proving them wrong, and probably some of them would (again) follow me around the net, stirring up trouble. BTDT. So thanks, but no thanks. – Cheers and hth. - Alf Dec 14 '13 at 04:30
  • @Cheersandhth.-Alf: I see, okay sure. Would you like to post this as an answer on this question at least? I'd accept it... – user541686 Dec 14 '13 at 04:32
  • @Mehrdad: okay, done. – Cheers and hth. - Alf Dec 14 '13 at 04:53

2 Answers2

4

First, regarding …

“ I have repeatedly read that inheriting from STL containers is a Bad Thing.”

… with the reference providing the reason that STL containers do not have virtual destructors.

It's certainly good advice for novices to not derive from classes without virtual destructors. That makes them unable to e.g. access the protected member in std::stack, it makes them unable to use Microsoft COM technology, etc., but all in all, for the novice the net advantages are tremendous. Likewise we advice novices to not use raw arrays and direct new and delete, which when followed can be a tremendous net advantage for the novice, but still, some – more experienced – have to do it, have to implement the abstractions that the novices are (ideally) constrained to use, lest there be no dynamic allocation in any C++ program.

So, with classes like std::stack clearly demonstrating that an absolute rule about this is nonsense, the experienced programmer must weight the pros and cons, and when deciding on inheritance, must choose between public, protected and private inheritance.

Con public inheritance: if a novice allocates an auto_vector dynamically, and then attempts to destroy it by using delete on a pointer to std::vector, then the design has failed to guide the novice to correct usage. So if it is a goal to strongly guide novices to correct usage, then either don't do this, or also add functionality that makes dynamic allocation difficult for novices. E.g. add an operator new with additional argument of inaccessible type, or itself inaccessible.

For the case at hand other polymorphic access as std::vector is not a problem, because code that indexes a std::vector beyond its size already has Undefined Behavior.

For other cases, cases that one can imagine, that are different from the OP's case, one must consider the particulars of those other cases: general absolute rules don't cut it in programming (well, at least not in C++ programming).

Pro public inheritance in this case: it's, well, the easiest and most direct and clear expression of "I want all the base class' functionality".

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
1

The lack of the virtual destructor in std::vector is a sign you should not take lightly.

Composition is almost always better than inheritance to extend a type. What you should do in your case is to compose an std::vector and only re-implement exactly the member functions you'll want to use on it. Shouldn't really be a big issue in the end since one seldom uses more than 5-10 of the entire public interface of std::vector for a given usecase. The re-implemented functions simply forward their calls to the composed std::vector and your new operator[] adds the additional resizing logic.

By doing that, there's no way of accidentally using your new type badly as a std::vector.

Johann Gerell
  • 24,991
  • 10
  • 72
  • 122
  • When the design at hand allows it, it's much simpler and guranteed efficient to use `private` inheritance and `using` directives to make members public. I didn't downvote the claim of "need to" even though it's IMO wrong, because I think downvoting should be restricted to clearly incorrect technical stuff. Still. – Cheers and hth. - Alf Dec 14 '13 at 04:10
  • @Mehrdad: Agree, removed that section. – Johann Gerell Dec 14 '13 at 04:18