Зачем нам нужен чистый виртуальный деструктор в С++?

Я понимаю необходимость виртуального деструктора. Но зачем нам нужен чистый виртуальный деструктор? В одной из статей С++ автор упомянул, что мы используем чистый виртуальный деструктор, когда хотим сделать абстрактное класс.

Но мы можем сделать реферат класса, сделав любую из функций-членов чистой виртуальной.

Итак, мои вопросы

  • Когда мы действительно делаем деструктор чистым виртуальным? Может ли кто-нибудь дать хороший пример в реальном времени?

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

Ответ 1

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

  • Нет, простого старого виртуального достаточно.

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

Обратите внимание, что поскольку компилятор будет генерировать неявный деструктор для производных классов, если автор класса этого не делает, любые производные классы будут не абстрактными. Поэтому наличие чистого виртуального деструктора в базовом классе не будет иметь никакого значения для производных классов. Это приведет только к абстрактному базовому классу (спасибо за комментарий @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.
};

Ответ 2

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

Ответ 3

Если вы хотите создать абстрактный базовый класс:

  • что не может быть создан (да, это избыточно с термином "абстрактный"!)
  • но требуется поведение виртуального деструктора (вы намерены переносить указатели на ABC, а не указатели на производные типы и удалять их)
  • но не требует никакого другого поведения виртуальной диспетчеризации для других методов (возможно, нет других методов: рассмотрим простой защищенный "ресурсный" контейнер, который нуждается в конструкторе/деструкторе/присваивании, но не намного больше )

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

Для нашей гипотетической ABC:

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

Ответ 4

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

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

По-моему, полезны чистые виртуальные деструкторы. Например, предположим, что у вас есть два класса myClassA и myClassB в вашем коде и что myClassB наследует от myClassA. По причинам, упомянутым Скоттом Мейерсом в его книге "Более эффективный С++", пункт 33 "Внедрение неклассических классов", лучше всего на самом деле создать абстрактный класс myAbstractClass, из которого наследуются myClassA и myClassB. Это обеспечивает лучшую абстракцию и предотвращает некоторые проблемы, возникающие, например, при копировании объектов.

В процессе абстракции (создания класса myAbstractClass) может быть, что ни один метод myClassA или myClassB не является хорошим кандидатом на то, чтобы быть чистым виртуальным методом (что является предпосылкой для абстрактного характера myAbstractClass). В этом случае вы определяете абстрактный деструктор класса pure virtual.

В дальнейшем конкретный пример из некоторого кода, который я сам написал. У меня есть два класса, 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; 
};

Ответ 5

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

Ответ 6

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

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

    База * pBase = new Derived(); удалить pBase; чистый виртуальный деструктор не требуется, только рабочий деструктор выполнит эту работу.

Ответ 7

Вы попросили пример, и я считаю, что следующее дает причину для чистого виртуального деструктора. Я с нетерпением жду ответов на вопрос, является ли это веской причиной...

Я не хочу, чтобы кто-либо мог использовать тип 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>&) {
}

Ответ 8

Вы получаете ответы на гипотезы с этими ответами, поэтому я попытаюсь сделать более простое, более подробное объяснение для ясности.

Основными отношениями объектно-ориентированного проектирования являются два:  IS-A и HAS-A. Я этого не делал. Это то, что они называются.

IS-A указывает, что конкретный объект идентифицирует себя как класс, который находится над ним в иерархии классов. Банановый объект - это плодовой объект, если он является подклассом класса фруктов. Это означает, что в любом месте класса фруктов можно использовать банан. Однако это не рефлексивно. Вы не можете подставить базовый класс для определенного класса, если для этого требуется специальный класс.

Has-a указал, что объект является частью составного класса и что существует отношение собственности. Это означает, что в С++ это объект-член и, как таковой, он принадлежит классу-хозяину, чтобы избавиться от него или отдать права собственности, прежде чем уничтожить себя.

Эти два понятия легче реализовать в языках с одним наследованием, чем в модели множественного наследования типа С++, но правила по существу одинаковы. Усложнение возникает, когда идентификатор класса неоднозначен, например, передача указателя класса Banana в функцию, которая принимает указатель класса Fruit.

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

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

Класс Fruit может иметь виртуальную функцию color(), которая по умолчанию возвращает "NONE". Функция color() функции Banana возвращает "ЖЕЛТЫЙ" или "BROWN".

Но если функция, принимающая указатель Fruit, вызывает color() в классе Banana, посланном ему - какая функция color() вызывается? Функция обычно вызывает Fruit:: color() для объекта Fruit.

То, что 99% времени не было тем, что предназначалось. Но если Fruit:: color() был объявлен виртуальным, тогда для объекта будет вызываться Banana: color(), потому что правильная функция color() будет привязана к указателю Fruit во время вызова. Время выполнения проверяет, на какой объект указывает указатель, поскольку он был отмечен как виртуальный в определении класса Fruit.

Это отличается от переопределения функции в подклассе. В таком случае указатель Fruit вызовет Fruit:: color(), если все, что он знает, это указатель IS-A на Fruit.

Итак, теперь возникает идея "чистой виртуальной функции". Это довольно печальная фраза, поскольку чистота не имеет к этому никакого отношения. Это означает, что предполагается, что метод базового класса никогда не будет вызван. На самом деле чистую виртуальную функцию нельзя назвать. Однако он все равно должен быть определен. Должна существовать сигнатура функции. Многие кодировщики выполняют пустую реализацию {} для полноты, но компилятор будет генерировать один из них, если нет. В этом случае, когда функция вызывается, даже если указателем является Fruit, будет вызываться Banana:: color(), поскольку это единственная реализация цвета().

Теперь последний фрагмент головоломки: конструкторы и деструкторы.

Чистые виртуальные конструкторы являются незаконными, полностью. Это просто.

Но чистые виртуальные деструкторы работают в том случае, если вы хотите запретить создание экземпляра базового класса. Только суб-классы могут быть созданы, если деструктор базового класса является чисто виртуальным. соглашение должно назначить его 0.

 virtual ~Fruit() = 0;  // pure virtual 
 Fruit::~Fruit(){}      // destructor implementation

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

Таким образом, вам запрещено в этом случае создавать экземпляры Fruit, но разрешено создавать экземпляры Banana.

Вызов для удаления указателя Fruit, указывающего на экземпляр банана сначала вызовет Banana:: ~ Banana(), а затем вызовет Fuit:: ~ Fruit(), всегда. Потому что независимо от того, что, когда вы вызываете деструктор подкласса, должен следовать деструктор базового класса.

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

Если вы пишете С++, чтобы передавать только точные указатели классов без общих или неоднозначных указателей, тогда виртуальные функции действительно не нужны. Но если вам требуется гибкость во время выполнения (как в Apple Banana Orange == > Fruit), функции становятся проще и универсальнее с меньшим избыточным кодом. Вам больше не нужно писать функцию для каждого типа фруктов, и вы знаете, что каждый фрукт будет реагировать на color() с помощью своей правильной функции.

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

Ответ 9

Возможно, существует еще один REAL USE-CASE чистого виртуального деструктора, который я на самом деле не вижу в других ответах:)

Сначала я полностью согласен с выраженным ответом: это потому, что для запрещения чистого виртуального деструктора потребуется дополнительное правило в спецификации языка. Но это еще не тот случай, который Марк призывает:)

Сначала представьте это:

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() использовался "специальный" метод (деструктор). Но причина, по которой это чистый виртуальный, по-прежнему остается неизменной - для этого метода нет кода по умолчанию. Немного запутанным может быть тот факт, что ДОЛЖЕН быть каким-то деструктором, а компилятор действительно генерирует для него пустой код. Но с точки зрения программиста чистая виртуальность по-прежнему означает: "У меня нет кода по умолчанию, он должен предоставляться производными классами".

Я думаю, что здесь нет никакой большой идеи, просто объяснение, что чистая виртуальность работает очень равномерно - также для деструкторов.

Ответ 10

Это тема десятилетней давности:) Прочтите последние 5 параграфов пункта № 7 книги "Эффективный С++" для деталей, начиная с "Иногда может быть удобно дать классу чистый виртуальный деструктор..."

Ответ 11

1) Если требуется, чтобы производные классы выполняли очистку. Это редко.

2) Нет, но вы хотите, чтобы он был виртуальным.

Ответ 12

нам нужно сделать виртуальную виртуальную машину destructor из того факта, что если мы не создадим виртуальный деструктор, то компилятор только уничтожит содержимое базового класса, n все производные классы останутся неизменными, bacuse-компилятор не будет вызывать деструктор любого другого класса, кроме базового класса.