2012-05-07 4 views
18

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

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

ответ

28

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

Вы получаете неопределенное поведение, если пытаетесь удалить объект своего типа с помощью указателя на 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); 
} 
+9

+1 Всегда предпочитайте композицию вместо наследования. Еще хотелось бы, чтобы был какой-то способ уменьшить весь код шаблона, необходимый для упаковки. – daramarak

+0

@ daramarak: так ли я, если только что-то вроде 'using attribute.insert;' мог бы работать! С другой стороны, довольно редко вам нужны все методы, и обертка дает возможность дать осмысленное имя и использовать типы более высокого уровня :) –

+3

@daramarak: * По-прежнему хотелось бы, чтобы какой-то способ уменьшить весь шаблонный код необходимо для упаковки *: да, есть: наследование. Но программисты уверены в себе, что не должны использовать его ... потому что они всегда склонны интерпретировать его как «есть». Но это не требование, просто общественное признание. –

13

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

Почему?

Есть два традиционные причины для наследования:

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

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


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

  • состава: вы создаете новый объект, который содержит в std::map, и предоставить достаточный интерфейсу
  • расширение: создавать новые штрафные функции, которые работают на std::map

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

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

Это зависит от вас, чтобы решить, какой из этих двух подходов более подходит.

12

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

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

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

#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() 

делая вкладывается в B с

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

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

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

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

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

просто: по той же причине, вызывающей A::hello не приведет вызов B::hello, вызывающей A::~A() (на удалении) не приведет к B::~B(). Если вы можете принять -в вас программирование style- первое утверждение, нет причин, по которым вы не можете принять второй. В моем примере нет 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 (независимо от того, встроена ли она или унаследована) в любом случае является частью B.


После Luchian комментарии: Неопределенное поведение намекал выше в своих комментариях связано с удалением на указатель на ан-object's базы, без виртуального деструктора.

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

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

Мое положение просто означает, что C++ - это не только ООП, обязательно обслуживание для замены ООП.

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

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

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

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

+3

Вы делаете там некоторые опасные заявления. Не расценивайте необходимость уничтожения виртуального деструктора. В стандарте ** четко указано **, что неопределенное поведение возникает в ситуации, о которой я упоминал. Абстракция - большая часть ООП. Это означает, что вы не только получаете повторное использование, но и скрываете фактический тип. В хорошем дизайне, если вы используете наследование, вы получите «std :: map *», который фактически указывает на «MyMap». И если вы удалите его, все может произойти, в том числе сбой. –

+4

@ LuchianGrigore: * В стандарте четко указано, что неопределенное поведение возникает в ситуации, о которой я упоминал. *. Правда, но это не та ситуация, о которой я говорил, а не тот, в котором находится OP. * Смысл, в хорошем дизайне, если вы используете наследование, вы получите std :: map *, что на самом деле указывает на MyMap *: это вообще FALSE и true только при использовании ООП с чистым указателем. Это точно, что мои образцы НЕ. Как вы объясните существование моих образцов, которые вообще не используют полиморфизм и указатели? –

+2

@LuchianGrigore: Во всяком случае, я думаю, что вы правы *: то, что я утверждаю, опасно, но не для правильности программы, но для культуры программирования на основе ООП! Но не беспокойтесь: ваша реакция ожидалась! –