Подтвердить что ты не робот

С++: Унаследовано от std:: map

Я хочу наследовать от std::map, но насколько я знаю, std::map не имеет никакого виртуального деструктора.

Таким образом, можно явно вызвать деструктор std::map в моем деструкторе, чтобы обеспечить правильное уничтожение объекта?

4b9b3361

Ответ 1

Деструктор действительно вызван, даже если он не виртуальный, но это не проблема.

Вы получаете поведение undefined, если вы пытаетесь удалить объект своего типа с помощью указателя на std::map.

Использовать композицию вместо наследования, контейнеры std не предназначены для наследования, а вы не должны.

Я предполагаю, что вы хотите расширить функциональность std::map (скажем, вы хотите найти минимальное значение), и в этом случае у вас есть две намного лучшие и юридические, опции:

1) Как и было предложено, вы можете использовать композицию вместо:

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

2) Свободные функции:

namespace MapFunctionality
{
    template<class K, class V>
    V getMin(const std::map<K,V> m);
}

Ответ 2

Я хочу наследовать от std::map [...]

Почему?

Наследуются две традиционные причины:

  • для повторного использования его интерфейса (и, следовательно, методов, закодированных против него)
  • для повторного использования его поведения

Первый здесь не имеет смысла, поскольку map не имеет никакого метода virtual, поэтому вы не можете изменять его поведение путем наследования; и последнее является извращением использования наследования, которое только усложняет обслуживание в конце.


Без четкого представления о вашем предполагаемом использовании (отсутствии контекста в вашем вопросе) я буду предполагать, что вы действительно хотите предоставить контейнер, подобный карте, с некоторыми бонусами. Существует два способа добиться этого:

  • : вы создаете новый объект, содержащий std::map, и предоставляете соответствующий интерфейс
  • extension: вы создаете новые свободные функции, которые работают с std::map

Последнее проще, однако оно также более открыто: исходный интерфейс std::map по-прежнему широко открыт; поэтому он не подходит для ограничения операций.

Первый - более тяжелый, без сомнения, но предлагает больше возможностей.

Вам решать, какой из этих двух подходов более подходит.

Ответ 3

Существует заблуждение: наследование - вне концепции чистого ООП, что С++ не является - это не что иное, как "состав с неназванным членом с возможностью распада".

Отсутствие виртуальных функций (и деструктор не является особым, в этом смысле) делает ваш объект не полиморфным, но если то, что вы делаете, это просто "повторно использовать его поведение и выставлять собственный интерфейс". Наследование делает именно то, что вы просили.

Деструкторы не должны быть явно вызваны друг от друга, так как их вызов всегда привязан по спецификации.

#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;
}

выводит

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

Создание A, встроенного в B с помощью

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

который будет выводить точно то же самое.

"Не выводить, если деструктор не является виртуальным", не является обязательным последствием С++, а просто общепринятым не написанным (в спецификации нет ничего особенного: кроме UB-вызова delete на базе) возникает до С++ 99, когда ООП с помощью динамического наследования и виртуальных функций является единственной программируемой парадигмой, поддерживаемой С++.

Конечно, многие программисты по всему миру сделали свои кости с такой школой (то же самое, что учат iostreams как примитивы, затем переходят к массиву и указателям, а на последнем уроке учитель говорит "о... tehre также является STL, который имеет вектор, строку и другие расширенные функции" ), и сегодня, даже если С++ стал многопартийной, по-прежнему настаивайте на этом чистом правиле ООП.

В моем примере A:: ~ A() не является виртуальным точно как A:: hello. Что это значит?

Простая: по той же причине вызов A::hello не приведет к вызову B::hello, вызов A::~A() (путем удаления) не приведет к B::~B(). Если вы можете принять в своем стиле программирования первое утверждение, нет причин, по которым вы не можете принять второй. В моем примере нет A* p = new B, который получит delete p, поскольку A:: ~ A не является виртуальным и Я знаю, что это означает.

Именно та же причина, которая не будет сделана, используя второй пример для B, A* p = &((new B)->a); с delete p;, хотя этот второй случай, совершенно двойственный с первым, выглядит неинтересным никем без видимых причин.

Единственная проблема - это "обслуживание" в том смысле, что если код yopur просматривается программистом OOP, он откажется от него, а не потому, что он сам по себе не прав, а потому, что ему сказали это сделать.

Фактически, "не получается, если деструктор не является виртуальным", потому что большинство программистов опасаются, что слишком много программистов, которые не знают, что они не могут вызвать delete на указателе на базу. (Извините, если это не вежливо, но после 30+ лет опыта программирования я не вижу никакой другой причины!)

Но ваш вопрос другой:

Вызов B:: ~ B() (по удалению или по завершению области) всегда приведет к A:: ~ A(), поскольку A (независимо от того, встроено или унаследовано) в любом случае является частью Б.


Следующие комментарии Luchian: поведение Undefined, указанное выше в его комментариях, связано с удалением на основе указателя на объект без виртуального деструктора.

Согласно школе ООП, это приводит к правилу "не выводится, если не существует виртуального деструктора".

Здесь я указываю, что причины этой школы зависят от того, что каждый объект, ориентированный на ООП, должен быть полиморфным, а все полиморфным должно быть адресовано указателем на базу, чтобы разрешить замену объекта. Сделав это утверждение, эта школа намеренно пытается сделать пустоту пересечением между производными и нереагируемыми, так что чистая программа ООП не будет испытывать этот UB.

Моя позиция, просто, допускает, что С++ - это не просто ООП, а не все объекты С++, которые должны быть ООП ориентированы по умолчанию, и, признавая ООП, не всегда является необходимой потребностью, также допускает, что наследование С++ не всегда обязательно обслуживание для замены ООП.

std:: map НЕ является полиморфным, поэтому он НЕ заменяется. MyMap тот же: НЕ полиморфный и НЕ заменяемый.

Он просто должен повторно использовать std:: map и выставить один и тот же интерфейс std:: map. И наследование - это просто способ избежать длинного шаблона перезаписанных функций, который просто вызывает повторное использование.

MyMap не будет иметь виртуального dtor, поскольку std:: map его не имеет. И этого - для меня достаточно сказать программисту на С++, что они не являются полиморфными объектами и которые не должны использоваться друг на друга.

Я должен признать, что эта позиция сегодня не разделяется большинством экспертов С++. Но я думаю (мое единственное личное мнение), это только из-за их истории, которые относятся к ООП как к догме, чтобы служить, а не из-за необходимости C++. Для меня С++ не является чистым языком ООП и не обязательно должен всегда следовать парадигме ООП в контексте, в котором ООП не соблюдается или требуется.