Является ли распространение базового класса опасным не виртуальным деструктором?

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

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 (который, как я знаю, не рекомендуется, но ради обсуждения просто нести его) и перегрузить +=, + оператор. -WeffС++ дает мне предупреждение, потому что std::string имеет не виртуальный деструктор, но имеет ли значение, если подкласс не имеет элементов и не нужно ничего делать в своем деструкторе?

FYI перегрузка += заключалась в правильном форматировании пути к файлу, поэтому класс пути можно использовать как:

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;

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

Ответ 1

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

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

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

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 на С++. Здесь вы можете использовать его.

// 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 с настраиваемым распределителем для разных типов, но вы не хотите, чтобы не поддающийся обработке

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

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

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

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

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


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

Ответ 3

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

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

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

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

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

Ответ 4

Это не ответ на ваш вопрос, а проблема, которую вы пытались решить (форматирование пути). Посмотрите boost::filesystem, который имеет лучший способ конкатенации путей:

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

Затем вы можете получить путь в виде строки как в нейтральной платформе, так и в форматах платформы.

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

Ответ 5

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

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

Ответ 6

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

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

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

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

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

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