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

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

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

Когда вызов удаляет на классе с невиртуальным деструктором с функциями членства (как класс 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;

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

12
задан Unihedron 11 August 2014 в 09:57
поделиться

6 ответов

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

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

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

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();
}

Другое допустимое использование, которое я могу придумать, связано с отсутствием определений типов шаблонов в 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 с настраиваемым распределителем для многих различных типов, но не хотите, чтобы в вашем коде были замусорены неподдерживаемые

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;
5
ответ дан 2 December 2019 в 06:26
поделиться

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

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

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

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

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

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

1
ответ дан 2 December 2019 в 06:26
поделиться

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

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

1
ответ дан 2 December 2019 в 06:26
поделиться

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

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

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

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


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

13
ответ дан 2 December 2019 в 06:26
поделиться

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

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

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

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

B* b = new B();
delete b;
C* c = new C()
delete c;
2
ответ дан 2 December 2019 в 06:26
поделиться

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

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

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

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

2
ответ дан 2 December 2019 в 06:26
поделиться
Другие вопросы по тегам:

Похожие вопросы: