Subclass/inherit standard containers?
Asked Answered
K

8

62

I often read this statements on Stack Overflow. Personally, I don't find any problem with this, unless I am using it in a polymorphic way; i.e. where I have to use virtual destructor.

If I want to extend/add the functionality of a standard container then what is a better way than inheriting one? Wrapping those container inside a custom class requires much more effort and is still unclean.

Knackwurst answered 24/7, 2011 at 10:6 Comment(2)
I would say it depends on what kind of extension you want to add.Follansbee
I don't know if this older question of mine is a duplicate, but sure is related :) #4353703Samadhi
S
41

Maybe many people here will not like this answer, but it is time for some heresy to be told and yes ... be told also that "the king is naked!"

All the motivation against the derivation are weak. Derivation is not different than composition. It's just a way to "put things together". Composition puts things together giving them names, inheritance does it without giving explicit names.

If you need a vector that has the same interface and implementation of std::vector plus something more, you can:

  • use composition and rewrite all the embedded object function prototypes implementing function that delegates them (and if they are 10000... yes: be prepared to rewrite all those 10000) or...

  • inherit it and add just what you need (and ... just rewrite constructors, until C++ lawyers will decide to let them be inheritable as well: I still remember 10 year ago zealot discussion about "why ctors cannot call each other" and why it is a "bad bad bad thing" ... until C++11 permitted it and suddenly all those zealots shut up!) and let the new destructor be non-virtual as it was in the original one.

Just like for every class that has some virtual method and some not, you know you cannot pretend to invoke the non-virtual method of derived by addressing the base, the same applies for delete. There is no reason just for delete to pretend any particular special care.

A programmer who knows that whatever is not virtual isn't callable addressing the base, also knows not to use delete on your base after allocating your derived.

All the "avoid this", "don't do that", always sound as "moralization" of something that is natively agnostic. All the features of a language exist to solve some problem. The fact a given way to solve the problem is good or bad depends on the context, not on the feature itself.

If what you're doing needs to serve many containers, inheritance is probably not the way (you have to redo for all). If it is for a specific case ... inheritance is a way to compose. Forget OOP purisms: C++ is not a "pure OOP" language, and containers are not OOP at all.

Stratigraphy answered 4/9, 2011 at 21:11 Comment(8)
You should not use composition unless you have a new invariant to enforce. But then, you have to write all those interface methods over anyway (since you have a new invariant to enforce!). If you don't have a new invariant, a namespace scope free function is completely appropriate and works much better than inheritance since it can work on multiple types generically. This is objective reuse, not opinion. I know of no one who said calling ctors was bad - it couldn't be done in the language, which is completely different. None of this morality, it's objective metrics of reuse.Goforth
I don't agree. OOP inheritance and OOP composition are different concept than C++ iheritance, C++ members. I can implement object inheritance with C++ members, as I can implement Object composition with C++ inheritance. The fact that C++ inheritance is mostly used ti implement Object inheritance and C++ members are mostly used to implement Object composition is just a common convention due to the language history. Object reuse is a concept tight to an OOP abstraction. Code reuse is a concept tight to language "typing". The two are not necessarily required to be the same.Stratigraphy
I agree with ex0du5 here... I would say that not that derivation is bad, but that there are almost always better solutions. In particular, if you want to implement an std::vector plus "some more functions", a better solution is to make these functions free-standing, potentially templated. This works better than composition or inheritance. In case you add data-members, you are very likely to add some invariants, in which case you'll need to modify vector's interface to enforce them...Clintonclintonia
@ybungalobill: Have you ever tryed doing both the ways? My suspect is that you are saying just what you've been told to do, and never did. Freestanding function are OK until you don't have to share "state" between them (that's what classes are for, after all).Stratigraphy
@EmilioGaravaglia: Oh yes, I did myself, and I have to maintain such code written by others on a daily basis. OOP is not such an important C++ feature, or it is used incorrectly. If there is a shared state with invariants (that you should hide), then it is OK and you should refer to ex0du5 answer. If there are no invariants, make it a struct with non-member functions. This is my opinion after many years struggling with the "traditional" approach, not because I was told so by anyone.Clintonclintonia
@ybungalobill: so what's the problem? The OP explicitly said EXTEND. If it hasn no new additional state to share between new methods, is not "extending" (IMHO). So if you have to EXTEND you can embed and rewrite the full set of function or inherit the innermost and add the outermost. No polymorphism is required here. So don't "struggle": just derive and save your "retyping all the 121 std::string public methods" and anyone will thank you. Everybody knows yourstring is not polymorphic with its base. What "maitenance problem" you are struggling with here?Stratigraphy
@EmilioGaravaglia: std::string is a well known example of a bad class. It was "extended" this way till it got 121 public methods, most of which need nothing but a contiguous memory region of chars. The result is that you cannot reuse, say find_first_of, for searching inside a memory mapped file, for example. And I have not said anything about "maintenance problems".Clintonclintonia
+1 for the bowl of fresh air. The only real argument against doing this is that the std lib is broken by design.Crossquestion
G
100

There are a number of reasons why this a bad idea.

First, this is a bad idea because the standard containers do not have virtual destructors. You should never use something polymorphically that does not have virtual destructors, because you cannot guarantee cleanup in your derived class.

Basic rules for virtual dtors

Second, it is really bad design. And there are actually several reasons it is bad design. First, you should always extend the functionality of standard containers through algorithms that operate generically. This is a simple complexity reason - if you have to write an algorithm for every container it applies to and you have M containers and N algorithms, that is M x N methods you must write. If you write your algorithms generically, you have N algorithms only. So you get much more reuse.

It is also really bad design because you are breaking a good encapsulation by inheriting from the container. A good rule of thumb is: if you can perform what you need using the public interface of a type, make that new behavior external to the type. This improves encapsulation. If it's a new behavior you want to implement, make it a namespace scope function (like the algorithms). If you have a new invariant to impose, use containment in a class.

A classic description of encapsulation

Finally, in general, you should never think about inheritance as a means to extend the behavior of a class. This is one of the big, bad lies of early OOP theory that came about due to unclear thinking about reuse, and it continues to be taught and promoted to this day even though there is a clear theory why it is bad. When you use inheritance to extend behavior, you are tying that extended behavior to your interface contract in a way that ties users hands to future changes. For instance, say you have a class of type Socket that communicates using the TCP protocol and you extend it's behavior by deriving a class SSLSocket from Socket and implementing the behavior of the higher SSL stack protocol on top of Socket. Now, let's say you get a new requirement to have the same protocol of communications, but over a USB line, or over telephony. You would need to cut and paste all that work to a new class that derives from a USB class, or a Telephony class. And now, if you find a bug, you have to fix it in all three places, which won't always happen, which means bugs will take longer and not always get fixed...

This is general to any inheritance hierarchy A->B->C->... When you want to use the behaviors you've extended in derived classes, like B, C, .. on objects not of the base class A, you've got to redesign or you are duplicating implementation. This leads to very monolithic designs that are very hard to change down the road (think Microsoft's MFC, or their .NET, or - well, they make this mistake a lot). Instead, you should almost always think of extension through composition whenever possible. Inheritance should be used when you are thinking "Open / Closed Principle". You should have abstract base classes and dynamic polymorphism runtime through inherited class, each will full implementations. Hierarchies shouldn't be deep - almost always two levels. Only use more than two when you have different dynamic categories that go to a variety of functions that need that distinction for type safety. In those cases, use abstract bases until the leaf classes, which have the implementation.

Goforth answered 18/8, 2011 at 15:46 Comment(7)
+1 : Composition > InheritanceSomething
Very good point about inheritance.Centreboard
++++++++++++1. The parts I like a lot: 1) Generic & orthogonal design; 2) If things can be done via public interface, keep that external; 3) Socket/SSL/USB example - I've seen lots of legacy code falling into this trapLeandra
If one bug introduced issues into all of the USB, telephony, and SSL classes, wouldn't that imply that it was in fact shared code that should have been part of a common base (and fixed in only one place)?Premonitory
@jarvisteve: there's no SSL class. there's SSLSocket, SSLUSB, ... All have a bug related to the copied SSL implementation. There's no common base class and you cannot have one. And yes, the whole point of fixing that is to have the code at one place.Baptize
Re: big, bad lies of early OOP theory - you are wrong: the confusion comes from misunderstanding the difference between generalization and abstraction; further you are not highlighting problems with OOD, but the limitations on OO imposed by C++'s simulation of OO - C++ is NOT OO!Backstage
@Backstage Eiffel himself discusses extension inheritance as legitimate and gives examples in line with the one I give. I’m not just misreading early lit - it was clearly an intended use across languages. Once we hit the 90’s, though, the issues with reuse began to be pointed out more widely, and the guidance became more constrained, particularly once we had the mathematical / categorial view of polymorphism and a descriptive intensional language.Goforth
S
41

Maybe many people here will not like this answer, but it is time for some heresy to be told and yes ... be told also that "the king is naked!"

All the motivation against the derivation are weak. Derivation is not different than composition. It's just a way to "put things together". Composition puts things together giving them names, inheritance does it without giving explicit names.

If you need a vector that has the same interface and implementation of std::vector plus something more, you can:

  • use composition and rewrite all the embedded object function prototypes implementing function that delegates them (and if they are 10000... yes: be prepared to rewrite all those 10000) or...

  • inherit it and add just what you need (and ... just rewrite constructors, until C++ lawyers will decide to let them be inheritable as well: I still remember 10 year ago zealot discussion about "why ctors cannot call each other" and why it is a "bad bad bad thing" ... until C++11 permitted it and suddenly all those zealots shut up!) and let the new destructor be non-virtual as it was in the original one.

Just like for every class that has some virtual method and some not, you know you cannot pretend to invoke the non-virtual method of derived by addressing the base, the same applies for delete. There is no reason just for delete to pretend any particular special care.

A programmer who knows that whatever is not virtual isn't callable addressing the base, also knows not to use delete on your base after allocating your derived.

All the "avoid this", "don't do that", always sound as "moralization" of something that is natively agnostic. All the features of a language exist to solve some problem. The fact a given way to solve the problem is good or bad depends on the context, not on the feature itself.

If what you're doing needs to serve many containers, inheritance is probably not the way (you have to redo for all). If it is for a specific case ... inheritance is a way to compose. Forget OOP purisms: C++ is not a "pure OOP" language, and containers are not OOP at all.

Stratigraphy answered 4/9, 2011 at 21:11 Comment(8)
You should not use composition unless you have a new invariant to enforce. But then, you have to write all those interface methods over anyway (since you have a new invariant to enforce!). If you don't have a new invariant, a namespace scope free function is completely appropriate and works much better than inheritance since it can work on multiple types generically. This is objective reuse, not opinion. I know of no one who said calling ctors was bad - it couldn't be done in the language, which is completely different. None of this morality, it's objective metrics of reuse.Goforth
I don't agree. OOP inheritance and OOP composition are different concept than C++ iheritance, C++ members. I can implement object inheritance with C++ members, as I can implement Object composition with C++ inheritance. The fact that C++ inheritance is mostly used ti implement Object inheritance and C++ members are mostly used to implement Object composition is just a common convention due to the language history. Object reuse is a concept tight to an OOP abstraction. Code reuse is a concept tight to language "typing". The two are not necessarily required to be the same.Stratigraphy
I agree with ex0du5 here... I would say that not that derivation is bad, but that there are almost always better solutions. In particular, if you want to implement an std::vector plus "some more functions", a better solution is to make these functions free-standing, potentially templated. This works better than composition or inheritance. In case you add data-members, you are very likely to add some invariants, in which case you'll need to modify vector's interface to enforce them...Clintonclintonia
@ybungalobill: Have you ever tryed doing both the ways? My suspect is that you are saying just what you've been told to do, and never did. Freestanding function are OK until you don't have to share "state" between them (that's what classes are for, after all).Stratigraphy
@EmilioGaravaglia: Oh yes, I did myself, and I have to maintain such code written by others on a daily basis. OOP is not such an important C++ feature, or it is used incorrectly. If there is a shared state with invariants (that you should hide), then it is OK and you should refer to ex0du5 answer. If there are no invariants, make it a struct with non-member functions. This is my opinion after many years struggling with the "traditional" approach, not because I was told so by anyone.Clintonclintonia
@ybungalobill: so what's the problem? The OP explicitly said EXTEND. If it hasn no new additional state to share between new methods, is not "extending" (IMHO). So if you have to EXTEND you can embed and rewrite the full set of function or inherit the innermost and add the outermost. No polymorphism is required here. So don't "struggle": just derive and save your "retyping all the 121 std::string public methods" and anyone will thank you. Everybody knows yourstring is not polymorphic with its base. What "maitenance problem" you are struggling with here?Stratigraphy
@EmilioGaravaglia: std::string is a well known example of a bad class. It was "extended" this way till it got 121 public methods, most of which need nothing but a contiguous memory region of chars. The result is that you cannot reuse, say find_first_of, for searching inside a memory mapped file, for example. And I have not said anything about "maintenance problems".Clintonclintonia
+1 for the bowl of fresh air. The only real argument against doing this is that the std lib is broken by design.Crossquestion
R
17

Publicly inheriting is a problem for all the reasons others have stated, namely that your container can be upcasted to the base class which does not have a virtual destructor or virtual assignment operator, which can lead to slicing problems.

Privately inheriting, on the other hand, is less of an issue. Consider the following example:

#include <vector>
#include <iostream>

// private inheritance, nobody else knows about the inheritance, so nobody is upcasting my
// container to a std::vector
template <class T> class MyVector : private std::vector<T>
{
private:
    // in case I changed to boost or something later, I don't have to update everything below
    typedef std::vector<T> base_vector;

public:
    typedef typename base_vector::size_type       size_type;
    typedef typename base_vector::iterator        iterator;
    typedef typename base_vector::const_iterator  const_iterator;

    using base_vector::operator[];

    using base_vector::begin;
    using base_vector::clear;
    using base_vector::end;
    using base_vector::erase;
    using base_vector::push_back;
    using base_vector::reserve;
    using base_vector::resize;
    using base_vector::size;

    // custom extension
    void reverse()
    {
        std::reverse(this->begin(), this->end());
    }
    void print_to_console()
    {
        for (auto it = this->begin(); it != this->end(); ++it)
        {
            std::cout << *it << '\n';
        }
    }
};


int main(int argc, char** argv)
{
    MyVector<int> intArray;
    intArray.resize(10);
    for (int i = 0; i < 10; ++i)
    {
        intArray[i] = i + 1;
    }
    intArray.print_to_console();
    intArray.reverse();
    intArray.print_to_console();

    for (auto it = intArray.begin(); it != intArray.end();)
    {
        it = intArray.erase(it);
    }
    intArray.print_to_console();

    return 0;
}

OUTPUT:

1
2
3
4
5
6
7
8
9
10
10
9
8
7
6
5
4
3
2
1

Clean and simple, and gives you the freedom to extend std containers without much effort.

And if you think about doing something silly, like this:

std::vector<int>* stdVector = &intArray;

You get this:

error C2243: 'type cast': conversion from 'MyVector<int> *' to 'std::vector<T,std::allocator<_Ty>> *' exists, but is inaccessible
Rau answered 28/9, 2016 at 18:58 Comment(0)
S
9

You should refrain from deriving publicly from standard contianers. You may choose between private inheritance and composition and it seems to me that all the general guidelines indicate that composition is better here since you don't override any function. Don't derive publicly form STL containers - there really isn't any need of it.

By the way, if you want to add a bunch of algorithms to the container, consider adding them as freestanding functions taking an iterator range.

Samadhi answered 24/7, 2011 at 10:11 Comment(10)
+1, interesting. This means that I can derive protected/privately ?Knackwurst
For me the argument "there really isn't any need of it" is a non-argument. Sorry ;). You don't really explain why one should not do it, what the technical reason for this is, rather than just saying "it is a bad practice because it is a bad practice"Tenaculum
@iammilind: Yes, you can, but composition should be preferred. See this gotw gotw.ca/publications/mill06.htm for discussion about containment vs nonpublic inheritanceSamadhi
@Joris: What is it that public inheritance of a contianer can solve that public inheritance and composition cannot?Samadhi
Again that is a question for which one should cover ALL possible scenario's in the world to have a conclusive answer. We never know that someday someone comes up with a valid reason to inherit instead of using composition in a very particular case. It is just a way of reasoning I do not like as it is based on gut feeling rather than 100% objective arguments.Tenaculum
@Joris: I agree that there can theoretically be cases when public inheritance perhaps is the best way. But I insist that all other possibilities must first be ruled out.Samadhi
@Armen, here Joris is correct. Suppose if you want to do some additional things in just vector::push_back() method then the best way is to inherit it. Composition solves that problem, but you have to do lot of work.Knackwurst
@iammilind: You do realize that you cannot override push_back, do you? Because it is not virtual. You can at best hide it. And you can achieve the same by both composition and private inheritance with little or no additional effortSamadhi
Just for clarification's sake, I would never try to inherit a stl container, as my gut feeling tells me it is a bad idea, but I like the topicstarter's question. It would be nice to have the technical reason for it instead of just a "you shouldn't".Tenaculum
@Joris: The reason is that in order to use your derived class correctly you should be more cautious than usual. And if there are two ways of doing the same - one that requires you to be more cautious and one that doesn't - I think the latter should be preferred as per a variation of Occham's razor :)Samadhi
F
5

The problem is that you, or someone else, might accidentally pass your extended class to a function expecting a reference to the base class. That will effectively (and silently!) slice off the extensions and create some hard to find bugs.

Having to write some forwarding functions seems like a small price to pay in comparison.

Farsighted answered 24/7, 2011 at 10:41 Comment(3)
How about private inheritance ?Knackwurst
@iammilind: If you inherit privately, you can't pass the inherited class to said function. The base class will be inaccessible. - A privately inherited class is not the base class, and inheritance will be just an implementation detail.Jojo
Great answer! Why when someone asks about inhereting from value types no one mentions slicing?Diehard
C
2

Because you can never guarantee that you haven't used them in a polymorphic way. You're begging for problems. Taking the effort to write a few functions is no big deal, and, well, even wanting to do this is dubious at best. What happened to encapsulation?

Condense answered 24/7, 2011 at 10:12 Comment(0)
J
2

Most common reason to want to inherit from the containers is because you want to add some member function to the class. Since stdlib itself is not modifiable, inheritance is thought to be the substitute. This does not work however. It's better to do a free function that takes a vector as parameter:

void f(std::vector<int> &v) { ... }
Jacquline answered 4/9, 2011 at 14:36 Comment(0)
K
-3

IMHO, I don't find any harm in inheriting STL containers if they are used as functionality extensions. (That's why I asked this question. :) )

The potential problem can occur when you try to pass the pointer/reference of your custom container to a standard container.

template<typename T>
struct MyVector : std::vector<T> {};

std::vector<int>* p = new MyVector<int>;
//....
delete p; // oops "Undefined Behavior"; as vector::~vector() is not 'virtual'

Such problems can be avoided consciously, provided good programming practice is followed.

If I want to take extreme care then I can go upto this:

#include<vector>
template<typename T>
struct MyVector : std::vector<T> {};
#define vector DONT_USE

Which will disallow using vector entirely.

Knackwurst answered 3/9, 2011 at 7:17 Comment(1)
#defining a name from the standard library is undefined behavior. Try including some other standard header after whose implementation (legitimately) uses a vector. Good luck. :)Clintonclintonia

© 2022 - 2024 — McMap. All rights reserved.