Опасно ли расширение базового класса не виртуальным деструктором? - PullRequest
12 голосов
/ 08 апреля 2010

В следующем коде:

class A {
};
class B : public A {
};
class C : public A {
   int x;
};

int main (int argc, char** argv) {
   A* b = new B();
   A* c = new C();

   //in both cases, only ~A() is called, not ~B() or ~C()
   delete b; //is this ok?
   delete c; //does this line leak memory?

   return 0;
}

При вызове delete для класса с не виртуальным деструктором с функциями-членами (такими как класс C) может ли распределитель памяти сказать, каков правильный размер объекта? Если нет, то утечка памяти?

Во-вторых, если у класса нет функций-членов и нет явного поведения деструктора (например, класса B), все ли в порядке?

Я спрашиваю об этом, потому что я хотел создать класс для расширения std::string (что, я знаю, не рекомендуется, но ради обсуждения просто терпеть), и перегрузить +=, + оператор. -Weffc ++ дает мне предупреждение, потому что std::string имеет не виртуальный деструктор, но имеет ли значение, если у подкласса нет членов и ему ничего не нужно делать в своем деструкторе?

К вашему сведению, перегрузка += должна была выполнить правильное форматирование пути к файлу, поэтому класс пути можно использовать как:

class path : public std::string {
    //... overload, +=, +
    //... add last_path_component, remove_path_component, ext, etc...
};

path foo = "/some/file/path";
foo = foo + "filename.txt";
std::string s = foo; //easy assignment to std::string
some_function_taking_std_string (foo); //easy implicit conversion
//and so on...

Я просто хотел убедиться, что кто-то делает это:

path* foo = new path();
std::string* bar = foo;
delete bar;

не вызовет проблем с выделением памяти?

Ответы [ 6 ]

13 голосов
/ 08 апреля 2010

Нет, публично наследовать от классов без виртуальных деструкторов небезопасно, потому что, если вы удаляете производную через базу, вы вводите неопределенное поведение.Определение производного класса не имеет значения (члены данных или нет и т. Д.):

§5.3.5 / 3: в первом варианте (удаление объекта), если статический тип операндаотличается от своего динамического типа, статический тип должен быть базовым классом динамического типа операнда , а статический тип должен иметь виртуальный деструктор, или поведение не определено. (выделение мое)1007 *

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

Это (одна из причин *), почему вы не должны наследовать от стандартных библиотечных классов. Лучшее решение - расширить его с помощью свободных функций.На самом деле, даже если бы вы могли, вы все равно должны предпочесть бесплатные функции .


* Еще одно: вы действительно хотите заменить все использование строк новымкласс строки, просто чтобы получить некоторые функциональные возможности?Это много ненужной работы.

5 голосов
/ 08 апреля 2010

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

Тем не менее, это обычно считается плохой вещью, поскольку кто-то может попытаться создать ее полиморфно в более поздний срок. (Вы можете быть в состоянии защититься от этого с помощью нового нереализованного частного оператора, но я не уверен.)

У меня есть два примера, в которых я не ненавижу выводить из классов с не виртуальными деструкторами. Первый - создание синтаксического сахара с использованием временных ... вот надуманный пример.

class MyList : public std::vector<int>
{
   public:
     MyList operator<<(int i) const
     {
       MyList retval(*this);
       retval.push_back(i);
       return retval;
     }
   private: 
     // Prevent heap allocation
     void * operator new   (size_t);
     void * operator new[] (size_t);
     void   operator delete   (void *);
     void   operator delete[] (void*);
};

void do_somthing_with_a_vec( std::vector<int> v );
void do_somthing_with_a_const_vec_ref( const std::vector<int> &v );

int main()
{
   // I think this slices correctly .. 
   // if it doesn't compile you might need to add a 
   // conversion operator to MyList
   std::vector<int> v = MyList()<<1<<2<<3<<4;

  // This will slice to a vector correctly.
   do_something_with_a_vec( MyList()<<1<<2<<3<<4 );

  // This will pass a const ref - which will be OK too.
   do_something_with_a_const_vec_ref( MyList()<<1<<2<<3<<4 );

  //This will not compile as MyList::operator new is private
  MyList * ptr = new MyList();
}

Другое допустимое использование, о котором я могу подумать, связано с отсутствием шаблонных typedefs в C ++. Вот как это можно использовать.

// Assume this is in code we cant control
template<typename T1, typename T2 >
class ComplicatedClass
{
  ...
};

// Now in our code we want TrivialClass = ComplicatedClass<int,int>
// Normal typedef is OK
typedef ComplicatedClass<int,int> TrivialClass;

// Next we want to be able to do SimpleClass<T> = ComplicatedClass<T,T> 
// But this doesn't compile
template<typename T>
typedef CompilicatedClass<T,T> SimpleClass;

// So instead we can do this - 
// so long as it is not used polymorphically if 
// ComplicatedClass doesn't have a virtual destructor we are OK.
template<typename T>
class SimpleClass : public ComplicatedClass<T,T>
{
  // Need to add the constructors we want here :(
  // ...
   private: 
     // Prevent heap allocation
     void * operator new   (size_t);
     void * operator new[] (size_t);
     void   operator delete   (void *);
     void   operator delete[] (void*);
}

Вот более конкретный пример этого. Вы хотите использовать std :: map с пользовательским распределителем для многих различных типов, но вы не хотите, чтобы unmaintainable

std::map<K,V, std::less<K>, MyAlloc<K,V> >

замусорил твой код.

template<typename K, typename V>
class CustomAllocMap : public std::map< K,V, std::less<K>, MyAlloc<K,V> >
{
  ...
   private: 
     // Prevent heap allocation
     void * operator new   (size_t);
     void * operator new[] (size_t);
     void   operator delete   (void *);
     void   operator delete[] (void*);
}; 

MyCustomAllocMap<K,V> map;
2 голосов
/ 08 апреля 2010

Это не ответ на ваш вопрос, а проблема, которую вы пытались решить (форматирование пути). Взгляните на boost::filesystem, в котором есть лучший способ объединения путей:

boost::filesystem::path p = "/some/file/path";
p /= "filename.txt";

Затем вы можете получить путь в виде строки как в не зависящем от платформы, так и в специфическом для платформы формате.

Самое приятное то, что он был принят в TR2, что означает, что он станет частью стандарта C ++ в будущем.

2 голосов
/ 08 апреля 2010

Проблема может возникнуть, если вы храните адрес памяти производного типа внутри базового типа, а затем вызываете delete для базового типа:

B* b = new C();
delete b;

Если бы у B был виртуальный деструктор, то вызывался бы деструктор C, а затем B. Но без виртуального деструктора у вас неопределенное поведение.

следующие 2 удаления не вызывают проблем:

B* b = new B();
delete b;
C* c = new C()
delete c;
1 голос
/ 08 апреля 2010

Чтобы добавить к тому, что было сказано.

  • Вы на самом деле рассматриваете возможность добавления методов в класс, который уже считается раздутым.
  • Вы хотите, чтобы ваш path класс считался string, хотя ряд операций не имел бы смысла

Я бы предложил использовать Composition вместо Inheritance. Таким образом, вы будете переопределять (пересылать) только те операции, которые действительно имеют смысл для вашего класса (например, я не думаю, что вам действительно нужен оператор индекса).

Кроме того, вы можете рассмотреть возможность использования std::wstring или, возможно, строки ICU, чтобы иметь возможность обрабатывать больше, чем символы ASCII (я придирка, но тогда я француз, и более низкого ASCII недостаточно для Французский язык).

Это действительно вопрос инкапсуляции. Если вы решите правильно обрабатывать символы UTF-8 и изменить свой базовый класс ... скорее всего, ваши клиенты повесят вас за ноги. С другой стороны, если вы использовали композицию, у них никогда не возникнет проблем, если вы будете осторожно работать с интерфейсом.

Наконец, как было предложено, свободные функции должны идти впереди. Еще раз, потому что они обеспечивают лучшую инкапсуляцию (если они не друзья ...).

1 голос
/ 08 апреля 2010

Безопасно личное наследовать от базовых классов без виртуального деструктора.Открытое наследование позволило бы удалить производный класс через указатель базового класса, что является неопределенным поведением в C ++.

Это одно из единственно разумных применений частного наследования в C ++.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...