C++: Inheriting from std::map

2019-02-11 13:12发布

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?

4条回答
We Are One
2楼-- · 2019-02-11 13:25

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.

查看更多
看我几分像从前
3楼-- · 2019-02-11 13:32

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);
}
查看更多
成全新的幸福
4楼-- · 2019-02-11 13:39

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.

查看更多
Animai°情兽
5楼-- · 2019-02-11 13:45

@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).

查看更多
登录 后发表回答