C++: Inheriting from std::map
Asked Answered
S

5

28

I want to inherit from std::map, but as far as I know std::map hasn't any virtual destructor.

Is it therefore possible to call std::map's destructor explicitly in my destructor to ensure proper object destruction?

Shiksa answered 7/5, 2012 at 6:47 Comment(0)
T
37

The destructor does get called, even if it's not virtual, but that's not the issue.

You get undefined behavior if you attempt to delete an object of your type through a pointer to a std::map.

Use composition instead of inheritance, std containers are not meant to be inherited, and you shouldn't.

I'm assuming you want to extend the functionality of std::map (say you want to find the minimum value), in which case you have two far better, and legal, options:

1) As suggested, you can use composition instead:

template<class K, class V>
class MyMap
{
    std::map<K,V> m;
    //wrapper methods
    V getMin();
};

2) Free functions:

namespace MapFunctionality
{
    template<class K, class V>
    V getMin(const std::map<K,V> m);
}
Telesis answered 7/5, 2012 at 6:48 Comment(9)
+1 Always favour composition instead of inheritance. Still wish there was some way of reducing all the boilerplate code needed for wrapping.Scarcely
@daramarak: so do I, if only something like using attribute.insert; could work! On the other hand, it's pretty rare that you actually need all the methods, and wrapping gives the opportunity to give meaningful name and take higher-level types :)Execution
@daramarak: Still wish there was some way of reducing all the boilerplate code needed for wrapping: yes, there is: inheritance. But programmers are self-convinced they shouldn't use it... because they always tend to interpret it as "is a". But that's not a requirement, just a public convinction.Ethicize
C++ has private inheritance for is-implemented-by, and public inheritance for is-a.Hausmann
@MSalters: This is just the OOP interpretation of C++ inheritance mechanisms. But the OP context is not OOP oriented, so it has not to necessarily follow those rules.Ethicize
@Hausmann Private inheritance still means implementing the wrapper functions.Trimurti
@EmilioGaravaglia It isn't that "is a" is a conviction... it's that the std::map isn't designed for anyone to extend and should be marked as final.Trimurti
@srm: the standard documentation says nothing about that.Ethicize
@srm: or simply using Base::method;Hausmann
E
21

There is a misconception: inheritance -outside the concept of pure OOP, that C++ isn't - is nothing more than a "composition with an unnamed member, with a decay capability".

The absence of virtual functions (and the destructor is not special, in this sense) makes your object not polymorphic, but if what you are doing is just "reuse it behavior and expose the native interface" inheritance does exactly what you asked.

Destructors don't need to be explicitly called from each other, since their call is always chained by specification.

#include <iostream>
unsing namespace std;

class A
{
public:
   A() { cout << "A::A()" << endl; }
   ~A() { cout << "A::~A()" << endl; }
   void hello() { cout << "A::hello()" << endl; }
};

class B: public A
{
public:
   B() { cout << "B::B()" << endl; }
   ~B() { cout << "B::~B()" << endl; }
   void hello() { cout << "B::hello()" << endl; }
};

int main()
{
   B b;
   b.hello();
   return 0;
}

will output

A::A()
B::B()
B::hello()
B::~B()
A::~A()

Making A embedded into B with

class B
{
public:
   A a;
   B() { cout << "B::B()" << endl; }
   ~B() { cout << "B::~B()" << endl; }
   void hello() { cout << "B::hello()" << endl; }
};

that will output exactly the same.

The "Don't derive if the destructor is not virtual" is not a C++ mandatory consequence, but just a commonly accepted not written (there's nothing in the spec about it: apart an UB calling delete on a base) rule that arises before C++99, when OOP by dynamic inheritance and virtual functions was the only programming paradigm C++ supported.

Of course, many programmers around the world made their bones with that kind of school (the same that teach iostreams as primitives, then moves to array and pointers, and on the very last lesson the teacher says "oh ... tehre is also the STL that has vector, string and other advanced features") and today, even if C++ becamed multiparadigm, still insist with this pure OOP rule.

In my sample A::~A() isn't virtual exactly as A::hello. What does it mean?

Simple: for the same reason calling A::hello will not result in calling B::hello, calling A::~A() (by delete) will not result in B::~B(). If you can accept -in you programming style- the first assertion, there are no reason you cannot accept the second. In my sample there is no A* p = new B that will receive delete p since A::~A isn't virtual and I know what it means.

Exactly that same reason that will not make, using the second example for B, A* p = &((new B)->a); with a delete p;, although this second case, perfectly dual with the first one, looks not interesting anyone for no apparent reasons.

The only problem is "maintenance", in the sense that -if yopur code is viewed by an OOP programmer- will refuse it, not because it is wrong in itself, but because he has been told to do so.

In fact, the "don't derive if the destructor is not virtual" is because the most of the programmers beleave that there are too many programmers that don't know they cannot call delete on a pointer to a base. (Sorry if this is not polite, but after 30+ year of programming experience I cannot see any other reason!)

But your question is different:

Calling B::~B() (by delete or by scope ending) will always result in A::~A() since A (whether it is embedded or inherited) is in any case part-of B.


Following Luchian comments: the Undefined behavior alluded above an in his comments is related to a deletion on a pointer-to-an-object's-base with no virtual destructor.

According to the OOP school, this results in the rule "don't derived if no virtual destructor exist".

What I'm pointing out, here, is that the reasons of that school depends on the fact that every OOP oriented object has to be polymorphic and everything is polymorphic must be addressable by pointer to a base, to allow object substitution. By making those assertion, that school is deliberately trying in making void the intersection between derived and non-replacable, so that a pure OOP program will not experience that UB.

My position, simply, admits that C++ is not just OOP, and not all the C++ objects HAVE TO BE OOP oriented by default, and, admitting OOP is not always a necessary need, also admits that C++ inheritance is not always necessarily servicing to OOP substitution.

std::map is NOT polymorphic so it's NOT replaceable. MyMap is the same: NOT polymorphic and NOT replaceable.

It simply has to reuse std::map and expose the same std::map interface. And inheritance is just the way to avoid a long boilerplate of rewritten functions that just calls the reused ones.

MyMap will not have virtual dtor as std::map does not have one. And this -to me- is enough to tell a C++ programmer that these are not polymorphic objects and that must not be used one in the place of the other.

I have to admit this position is not today shared by the most of the C++ experts. But I think (my only personal opinion) this is just only because of their history, that relate to OOP as a dogma to serve, not because of a C++ need. To me C++ is not a pure OOP language and must not necessarily always follow the OOP paradigm, in a context where OOP is not followed or required.

Ethicize answered 7/5, 2012 at 8:21 Comment(8)
You're making some dangerous statements there. Don't regard the need for a virtual destructor as obsolete. The standard clearly states that undefined behavior arises in the situation I mentioned. Abstraction is a big part of OOP. That means you don't only derive to reuse, but also to hide the actual type. Meaning, in a good design, if you use inheritance, you'll end up with std::map* that actually points to MyMap. And if you delete it, anything can happen, including a crash.Telesis
@LuchianGrigore: The standard clearly states that undefined behavior arises in the situation I mentioned.. True, but this is not the situation I mentioned, and not the one the OP is in. * Meaning, in a good design, if you use inheritance, you'll end up with std::map* that actually points to MyMap*: that is in general FALSE, and true only with pure pointer based OOP. That is exactuy what my samples are NOT. How do you explain the existence of my samples, that don't use polymorphism and pointers at all?Ethicize
@LuchianGrigore: Anyway, I think you are correct: what I'm asserting IS dangerous, but not for program correctness, but for the OOP programming based culture! But don't worry: your reaction was expected!Ethicize
Then what's the point of the inheritance? Are you saying it's okay to inherit just to reuse the code, instead of having wrapper methods? OOP is much more than that. And if you don't have pointers to base classes, you're not abstracting enough. A good design is abstracted, loosely coupled, it delegates, etc. You're pointing him in a wrong direction.Telesis
@LuchianGrigore: Are you saying it's okay to inherit just to reuse the code, instead of having wrapper methods? I'm just saying "why not, if you're NOT DOING OOP?". OOP is much more than that. May be this will surprise you, but ... I KNOW. Perfectly. But I also know that OOP is not everything. if you don't have pointers to base classes, you're not abstracting enough.: the differnece between me and you is that I think what is "enough" should be defined by the context. Your position is legitimate, but that's not enough to make my one "wrong".Ethicize
Ok, fair enough. I think you should make the potential issues clear in your answer.Telesis
I know I'm late but my 2cents: From my perspective, a good design uses RAII wherever possible. Following this rule, I have rarely any occasion where I delete something via a virtual base pointer which was created somewhere else and where I do not know the derived type. Of course this is not true anymore if you happen to hand around shared_ptr<std::map> everywhere...Thermosetting
Late, too, but recently got into it with a fellow dev who just discovered the "don't inherit from std containers" mantra. In my case, all I wanted was a static resource to look up values quickly by string-based key. I usually do this by inheriting from a map (not necessarily STL) and in the constructor I pre-load the map. Instant dictionary--just for constructing one! And yes, I'm well aware of the "std::map doesn't have a virtual destructor" thing, but it is a map, and like you said, if map isn't polymorphic, then neither is my derivative.Carchemish
E
13

I want to inherit from std::map [...]

Why ?

There are two traditional reasons to inherit:

  • to reuse its interface (and thus, methods coded against it)
  • to reuse its behavior

The former makes no sense here as map does not have any virtual method so you cannot modify its behavior by inheriting; and the latter is a perversion of the use of inheritance which only complicates maintenance in the end.


Without a clear idea of your intended usage (lack of context in your question), I will suppose that what you really want is to provide a map-like container, with some bonus operations. There are two ways to achieve this:

  • composition: you create a new object, which contains a std::map, and provide the adequate interface
  • extension: you create new free-functions that operate on std::map

The latter is simpler, however it's also more open: the original interface of std::map is still wide-opened; therefore it is unsuitable for restricting operations.

The former is more heavyweight, undoubtedly, but offers more possibilities.

It's up to you to decide which of the two approaches is more suitable.

Execution answered 7/5, 2012 at 7:20 Comment(0)
P
3

@Matthieu M you said

I want to inherit from std::map [...]

Why ?

There are two traditional reasons to inherit:

  1. to reuse its interface (and thus, methods coded against it)
  2. to reuse its behavior

The former makes no sense here as map does not have any virtual method so you cannot modify its behavior by inheriting; and the latter is a perversion of the use of inheritance which only complicates maintenance in the end.

Regarding "the former":

The clear() function is virtual, and to me it makes a lot of sense for a std::map<key,valueClass*>::clear() to be overridden in a derived class with an iterator that deletes all the pointed to instances of the value class before calling the base class clear() to prevent accidental memory leaks, and it's a trick that I've actually used. As for why someone would want to use a map to pointers to classes, well polymorphism and references not being re-assignable means that the can't be used in a STL container. You might instead suggest the use of a reference_wrapper or smart pointer such as a shared_ptr (C++11 features) but when you're writing a library that you want somebody restricted to a C++98 compiler to be able to use, those aren't an option unless you're going to put a requirement on having boost, which can also be undesirable. And if you actually want the map to have sole ownership of its contents then you don't want to be using reference_wrapper or most implementations of smart pointers.

Regarding the "latter":

If you want a map to pointers that auto deletes pointed at memory, then reusing "all" other map behavior and overriding clear makes a lot of sense to me, of course then you'll also want to override assignment/copy constructors to clone the pointed to objects when you copy the map so that you don't double delete a pointed to instance of the valueClass.

But that's only requires an extremely small amount of coding to implement.

I also use a protected typedef std::map<key,valueClass*> baseClassMap; as the first 2 lines of the declaration of the derived class map, so that that that I can call baseClassMap::clear(); in the overridden clear() function after the iterator loop deletes all instances of valueClass* contained in the derived map, which make maintenance easier in case the type of valueClass* ever changes.

The point is, while it may have limited applicability in good coding practice, I don't think that it is fair to say that it is NEVER a good idea to descend from map. But maybe you have a better idea that I haven't thought of about how to achieve the same automatic memory management effect without adding a significant amount of additional source code (e.g. aggregating a std::map).

Prate answered 17/4, 2018 at 14:23 Comment(0)
T
2

ISO CPP Core Guidelines C.120 and C.129 clearly shows that inheritance is intended for either abstract interfaces or base classes that are intended to be complemented by derived classes. The former having pure virtual functions at the root of the hierarchy, and the latter often having non-virtual but protected functions to be used by derived classes.

ISO CPP Guidelines

I agree that inheritance for other uses can appear convenient. However, the direction of the designers of C++ is that the costs of code maintenance and the code design shortcomings of mis-using inheritance for other purposes, outweigh any convenience benefits.

Another alternative to a helper function or composition, is to use some of the lower-level pieces that map itself uses and create a new map class.

Turnstile answered 7/8, 2022 at 22:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.