Зачем нам нужен чистый виртуальный деструктор в C++?
Я понимаю необходимость виртуального деструктора. Но зачем нам чистый виртуальный деструктор? В одной из статей C++ автор упоминал, что мы используем чистый виртуальный деструктор, когда хотим сделать класс абстрактным.
Но мы можем сделать класс абстрактным, сделав любую функцию-член чисто виртуальной.
Так что мои вопросы
Когда мы действительно сделаем деструктор чисто виртуальным? Кто-нибудь может привести хороший пример в реальном времени?
Когда мы создаем абстрактные классы, полезно ли делать деструктор также чисто виртуальным? Если да, то почему?
12 ответов
Вероятно, настоящая причина, по которой разрешены чистые виртуальные деструкторы, заключается в том, что запретить их будет означать добавление еще одного правила к языку, и нет необходимости в этом правиле, поскольку от использования чистого виртуального деструктора не может быть никаких негативных последствий.
Нет, простого старого виртуального достаточно.
Если вы создаете объект с реализациями по умолчанию для его виртуальных методов и хотите сделать его абстрактным, не заставляя никого переопределять какой-либо конкретный метод, вы можете сделать деструктор чисто виртуальным. Я не вижу в этом особого смысла, но это возможно.
Обратите внимание, что, поскольку компилятор сгенерирует неявный деструктор для производных классов, если автор класса этого не сделает, любые производные классы не будут абстрактными. Следовательно, наличие чистого виртуального деструктора в базовом классе не будет иметь никакого значения для производных классов. Это только сделает базовый класс абстрактным (спасибо за комментарий kappa).
Можно также предположить, что каждому производному классу, вероятно, потребуется специальный код очистки и использование чистого виртуального деструктора в качестве напоминания для его написания, но это кажется надуманным (и неисполненным).
Примечание: деструктор является единственным методом, который, даже если он чисто виртуальный, должен иметь реализацию для создания экземпляров производных классов (да, чисто виртуальные функции могут иметь реализации).
struct foo {
virtual void bar() = 0;
};
void foo::bar() { /* default implementation */ }
class foof : public foo {
void bar() { foo::bar(); } // have to explicitly call default implementation.
};
Все, что вам нужно для абстрактного класса - это хотя бы одна чисто виртуальная функция. Подойдет любая функция; но, как это бывает, деструктор - это то, что есть у любого класса, поэтому он всегда в качестве кандидата. Более того, превращение деструктора в чисто виртуальный (в отличие от просто виртуального) не имеет никаких поведенческих побочных эффектов, кроме как сделать класс абстрактным. Таким образом, многие руководства по стилю рекомендуют последовательно использовать чистый виртуальный деструктор, чтобы указать, что класс является абстрактным, - если по какой-либо другой причине, кроме как он обеспечивает согласованное место, кто-то, читающий код, может посмотреть, является ли класс абстрактным.
Если вы хотите создать абстрактный базовый класс:
- это не может быть создано (да, это избыточно с термином "абстрактный"!)
- но требуется поведение виртуального деструктора (вы намерены переносить указатели на ABC, а не указатели на производные типы и удалять через них)
- но не требует какого-либо другого поведения виртуальной диспетчеризации для других методов (может быть , нет других методов? рассмотрим простой защищенный "ресурсный" контейнер, который нуждается в конструкторах / деструкторе / присваивании, но не более того)
... проще всего сделать класс абстрактным, сделав деструктор чисто виртуальным и предоставив для него определение (тело метода).
Для нашей гипотетической азбуки:
Вы гарантируете, что он не может быть создан (даже внутри самого класса, поэтому частных конструкторов может быть недостаточно), вы получаете виртуальное поведение, которое вы хотите для деструктора, и вам не нужно находить и маркировать другой метод, который не Виртуальная рассылка не требуется как "виртуальная".
Здесь я хочу сказать, когда нам нужен виртуальный деструктор и когда нам нужен чистый виртуальный деструктор
class Base
{
public:
Base();
virtual ~Base() = 0; // Pure virtual, now no one can create the Base Object directly
};
Base::Base() { cout << "Base Constructor" << endl; }
Base::~Base() { cout << "Base Destructor" << endl; }
class Derived : public Base
{
public:
Derived();
~Derived();
};
Derived::Derived() { cout << "Derived Constructor" << endl; }
Derived::~Derived() { cout << "Derived Destructor" << endl; }
int _tmain(int argc, _TCHAR* argv[])
{
Base* pBase = new Derived();
delete pBase;
Base* pBase2 = new Base(); // Error 1 error C2259: 'Base' : cannot instantiate abstract class
}
Если вы хотите, чтобы никто не мог создавать объект класса Base напрямую, используйте чистый виртуальный деструктор
virtual ~Base() = 0
, Обычно требуется хотя бы одна чисто виртуальная функция.virtual ~Base() = 0
, как эта функция.Когда вам не нужно вышеуказанное, нужно только безопасное уничтожение объекта класса Derived
Base* pBase = new Derived(); удалить pBase; чистый виртуальный деструктор не требуется, только виртуальный деструктор сделает эту работу.
Из ответов, которые я прочитал на ваш вопрос, я не смог найти вескую причину для фактического использования чистого виртуального деструктора. Например, следующая причина меня совсем не убеждает:
Вероятно, настоящая причина, по которой разрешены чистые виртуальные деструкторы, заключается в том, что запретить их будет означать добавление еще одного правила к языку, и нет необходимости в этом правиле, поскольку от использования чистого виртуального деструктора не может быть никаких негативных последствий.
На мой взгляд, чистые виртуальные деструкторы могут быть полезны. Например, предположим, что в вашем коде есть два класса myClassA и myClassB, и что myClassB наследуется от myClassA. По причинам, упомянутым Скоттом Мейерсом в его книге "Более эффективный C++", пункт 33 "Создание не листовых классов абстрактно", лучше на самом деле создать абстрактный класс myAbstractClass, от которого наследуются myClassA и myClassB. Это обеспечивает лучшую абстракцию и предотвращает некоторые проблемы, возникающие, например, с копиями объектов.
В процессе абстракции (создания класса myAbstractClass) может оказаться, что ни один метод myClassA или myClassB не является хорошим кандидатом на то, чтобы быть чисто виртуальным методом (что является обязательным условием для абстрактности myAbstractClass). В этом случае вы определяете деструктор абстрактного класса чисто виртуальный.
Далее конкретный пример из некоторого кода, который я сам написал. У меня есть два класса, Numerics/PhysicsParams, которые имеют общие свойства. Поэтому я позволил им наследовать от абстрактного класса IParams. В этом случае у меня не было абсолютно никакого метода, который мог бы быть чисто виртуальным. Например, метод setParameter должен иметь одинаковое тело для каждого подкласса. Единственный выбор, который у меня был, - сделать деструктор IParams чисто виртуальным.
struct IParams
{
IParams(const ModelConfiguration& aModelConf);
virtual ~IParams() = 0;
void setParameter(const N_Configuration::Parameter& aParam);
std::map<std::string, std::string> m_Parameters;
};
struct NumericsParams : IParams
{
NumericsParams(const ModelConfiguration& aNumericsConf);
virtual ~NumericsParams();
double dt() const;
double ti() const;
double tf() const;
};
struct PhysicsParams : IParams
{
PhysicsParams(const N_Configuration::ModelConfiguration& aPhysicsConf);
virtual ~PhysicsParams();
double g() const;
double rho_i() const;
double rho_w() const;
};
Если вы хотите остановить создание экземпляров базового класса без внесения каких-либо изменений в уже реализованный и протестированный производный класс, вы реализуете чистый виртуальный деструктор в базовом классе.
Вы получаете гипотезы с этими ответами, поэтому я попытаюсь сделать более простое, более приземленное объяснение для ясности.
Основными отношениями объектно-ориентированного проектирования являются два: IS-A и HAS-A. Я не сделал это. Вот как они называются.
IS-A указывает, что определенный объект идентифицируется как принадлежащий к классу, который находится над ним в иерархии классов. Банановый объект - это фруктовый объект, если он является подклассом фруктового класса. Это означает, что везде, где можно использовать фруктовый класс, можно использовать банан. Это не рефлексивно, хотя. Вы не можете заменить базовый класс для определенного класса, если этот конкретный класс вызывается.
Has-a указывает, что объект является частью составного класса и что существуют отношения собственности. В C++ это означает, что это объект-член, и поэтому ответственность за его уничтожение или передачу прав собственности перед уничтожением себя лежит на классе-владельце.
Эти два понятия легче реализовать в языках с одним наследованием, чем в модели множественного наследования, такой как C++, но правила по сути одинаковы. Сложность возникает, когда идентичность класса неоднозначна, например, передача указателя на класс Banana в функцию, которая принимает указатель на класс Fruit.
Виртуальные функции - это, во-первых, вещь во время выполнения. Он является частью полиморфизма в том смысле, что он используется для определения, какую функцию запускать во время ее вызова в работающей программе.
Ключевое слово virtual - это директива компилятора, связывающая функции в определенном порядке, если существует неопределенность в отношении идентичности класса. Виртуальные функции всегда находятся в родительских классах (насколько я знаю) и указывают компилятору, что связывание функций-членов с их именами должно осуществляться сначала с помощью функции подкласса, а после - с функцией родительского класса.
Класс Fruit может иметь виртуальную функцию color(), которая по умолчанию возвращает "NONE". Функция color() класса Banana возвращает "ЖЕЛТЫЙ" или "КОРИЧНЕВЫЙ".
Но если функция, принимающая указатель Fruit, вызывает color() для отправленного ей класса Banana - какая функция color() вызывается? Функция обычно вызывает Fruit::color() для объекта Fruit.
Это было бы в 99% случаев не тем, что предполагалось. Но если Fruit::color() был объявлен виртуальным, то для объекта будет вызван Banana:color(), поскольку правильная функция color() будет связана с указателем Fruit во время вызова. Среда выполнения проверит, на какой объект указывает указатель, поскольку он был помечен как виртуальный в определении класса Fruit.
Это отличается от переопределения функции в подклассе. В этом случае указатель Fruit будет вызывать Fruit::color(), если все, что он знает, это IS-A указатель на Fruit.
Так что теперь к идее "чисто виртуальной функции" подходит. Это довольно неудачная фраза, поскольку чистота не имеет к этому никакого отношения. Это означает, что предполагается, что метод базового класса никогда не будет вызываться. Действительно чисто виртуальная функция не может быть вызвана. Это все еще должно быть определено, как бы то ни было. Подпись функции должна существовать. Многие кодеры создают пустую реализацию {} для полноты, но компилятор сгенерирует ее внутренне, если нет. В том случае, когда функция вызывается, даже если указатель на Fruit, будет вызван Banana::color(), поскольку это единственная реализация color().
Теперь последний кусочек головоломки: конструкторы и деструкторы.
Чистые виртуальные конструкторы полностью запрещены. Это только что вышло.
Но чисто виртуальные деструкторы работают в том случае, если вы хотите запретить создание экземпляра базового класса. Только подклассы могут быть созданы, если деструктор базового класса является чисто виртуальным. соглашение состоит в том, чтобы присвоить его 0.
virtual ~Fruit() = 0; // pure virtual
Fruit::~Fruit(){} // destructor implementation
Вы должны создать реализацию в этом случае. Компилятор знает, что это то, что вы делаете, и удостоверяется, что вы все делаете правильно, или он сильно жалуется, что не может ссылаться на все функции, необходимые для компиляции. Ошибки могут сбивать с толку, если вы не на правильном пути относительно того, как вы моделируете иерархию классов.
Так что вам запрещено в этом случае создавать экземпляры Fruit, но разрешено создавать экземпляры Banana.
Вызов для удаления указателя Fruit, который указывает на экземпляр Banana, сначала вызовет Banana::~Banana(), а затем всегда вызовет Fuit::~Fruit(). Потому что, несмотря ни на что, когда вы вызываете деструктор подкласса, деструктор базового класса должен следовать.
Это плохая модель? Да, это более сложно на этапе проектирования, но оно может гарантировать, что правильное связывание выполняется во время выполнения и что функция подкласса выполняется там, где существует неопределенность в отношении того, к какому именно подклассу осуществляется доступ.
Если вы пишете на C++ так, что вы передаете только точные указатели классов без общих или неоднозначных указателей, то виртуальные функции на самом деле не нужны. Но если вам требуется гибкость типов во время выполнения (как в Apple Banana Orange ==> Fruit), функции становятся проще и универсальнее с меньшим количеством избыточного кода. Вам больше не нужно писать функцию для каждого типа фруктов, и вы знаете, что каждый фрукт будет отвечать на color() своей собственной правильной функцией.
Я надеюсь, что это многословное объяснение укрепляет концепцию, а не путает вещи. Есть много хороших примеров, на которые можно посмотреть, и посмотреть на достаточно, и на самом деле запустить их и связываться с ними, и вы получите это.
Это тема десятилетней давности:) Прочитайте последние 5 абзацев пункта № 7 книги "Эффективный C++" для подробностей, начиная с "Иногда бывает удобно дать классу чистый виртуальный деструктор...."
Вы просили привести пример, и я полагаю, что следующее дает причину для чисто виртуального деструктора. Я с нетерпением жду ответов относительно того, является ли это хорошей причиной...
Я не хочу, чтобы кто-то мог бросить error_base
тип, но типы исключений error_oh_shucks
а также error_oh_blast
имеют одинаковую функциональность, и я не хочу писать это дважды. Сложность pImpl необходима, чтобы избежать std::string
моим клиентам, и использование std::auto_ptr
требует копирования конструктора.
Открытый заголовок содержит спецификации исключений, которые будут доступны клиенту для различения различных типов исключений, создаваемых моей библиотекой:
// error.h
#include <exception>
#include <memory>
class exception_string;
class error_base : public std::exception {
public:
error_base(const char* error_message);
error_base(const error_base& other);
virtual ~error_base() = 0; // Not directly usable
virtual const char* what() const;
private:
std::auto_ptr<exception_string> error_message_;
};
template<class error_type>
class error : public error_base {
public:
error(const char* error_message) : error_base(error_message) {}
error(const error& other) : error_base(other) {}
~error() {}
};
// Neither should these classes be usable
class error_oh_shucks { virtual ~error_oh_shucks() = 0; }
class error_oh_blast { virtual ~error_oh_blast() = 0; }
И вот общая реализация:
// error.cpp
#include "error.h"
#include "exception_string.h"
error_base::error_base(const char* error_message)
: error_message_(new exception_string(error_message)) {}
error_base::error_base(const error_base& other)
: error_message_(new exception_string(other.error_message_->get())) {}
error_base::~error_base() {}
const char* error_base::what() const {
return error_message_->get();
}
Класс exception_string, хранящийся в тайне, скрывает std::string от моего открытого интерфейса:
// exception_string.h
#include <string>
class exception_string {
public:
exception_string(const char* message) : message_(message) {}
const char* get() const { return message_.c_str(); }
private:
std::string message_;
};
Мой код выдает ошибку как:
#include "error.h"
throw error<error_oh_shucks>("That didn't work");
Использование шаблона для error
немного безвозмездно. Это экономит немного кода за счет того, что клиенты должны отлавливать ошибки, как:
// client.cpp
#include <error.h>
try {
} catch (const error<error_oh_shucks>&) {
} catch (const error<error_oh_blast>&) {
}
Может быть, есть еще один НАСТОЯЩИЙ СЛУЧАЙ чистого виртуального деструктора, которого я на самом деле не вижу в других ответах:)
Сначала я полностью согласен с помеченным ответом: это потому, что запрет чистого виртуального деструктора потребует дополнительного правила в спецификации языка. Но это все еще не тот случай использования, к которому призывает Марк:)
Сначала представьте это:
class Printable {
virtual void print() const = 0;
// virtual destructor should be here, but not to confuse with another problem
};
и что-то вроде:
class Printer {
void queDocument(unique_ptr<Printable> doc);
void printAll();
};
Просто - у нас есть интерфейс Printable
и некоторый "контейнер", содержащий что-либо с этим интерфейсом. Я думаю, что здесь совершенно ясно, почему print()
метод чисто виртуальный. Он может иметь некоторое тело, но в случае отсутствия реализации по умолчанию, чисто виртуальная является идеальной "реализацией" (="должен быть предоставлен классом-потомком").
А теперь представьте точно так же, за исключением того, что это не для печати, а для уничтожения
class Destroyable {
virtual ~Destroyable() = 0;
};
А также может быть похожий контейнер:
class PostponedDestructor {
// Queues an object to be destroyed later.
void queObjectForDestruction(unique_ptr<Destroyable> obj);
// Destroys all already queued objects.
void destroyAll();
};
Это упрощенный вариант использования из моего реального приложения. Единственное отличие здесь в том, что вместо "нормального" использовался "специальный" метод (деструктор) print()
, Но причина, по которой он является чисто виртуальным, остается той же - нет кода по умолчанию для метода. Немного сбивает с толку тот факт, что ДОЛЖЕН быть какой-то деструктор, и компилятор фактически генерирует для него пустой код. Но с точки зрения программиста чистая виртуальность по-прежнему означает: "У меня нет никакого кода по умолчанию, он должен предоставляться производными классами".
Я думаю, что здесь нет никакой большой идеи, просто больше объяснений, что чистая виртуальность работает действительно единообразно - также для деструкторов.
Нам нужно сделать виртуальный деструктор из-за того факта, что, если мы не сделаем виртуальный деструктор, то компилятор только уничтожит содержимое базового класса, n все производные классы останутся неизменными, компилятор bacuse не вызовет деструктор любого другого класс кроме базового класса.
1) Когда вы хотите, чтобы производные классы выполняли очистку. Это редко.
2) Нет, но вы хотите, чтобы он был виртуальным.