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

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

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

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

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

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

12 ответов


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

  2. нет, простой старый виртуальный достаточно.

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

обратите внимание, что, поскольку компилятор создаст неявный деструктор для производных классов, если автор класса этого не сделает, любые производные классы будут не быть абстрактным. Поэтому наличие чистого виртуального деструктора в базовом классе не будет иметь никакого значения для производных классов. Это только сделает базу абстрактный класс (спасибо за @kappaкомментарий).

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

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

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

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


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

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

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

для нашей гипотетической азбуки:

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


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

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

в моем мнение, чистые виртуальные деструкторы могут быть полезны. Например, предположим, что в коде есть два класса myClassA и myClassB и что myClassB наследуется от myClassA. По причинам, упомянутым Скоттом Мейерсом в его книге "более эффективный C++", пункт 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; 
};

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


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

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
}
  1. Если вы хотите, чтобы никто не мог создать объект базового класса напрямую, используйте чистый виртуальный деструктор virtual ~Base() = 0. Обычно требуется хотя бы одна чистая виртуальная функция, возьмем virtual ~Base() = 0, как эта функция.

  2. когда вам не нужно над вещью, только вам нужен сейф уничтожение объекта производного класса

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


основные отношения объектно-ориентированного дизайна два: Есть-А и есть-А. Я их не выдумывал. Вот как они называются.

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

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

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

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

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

фруктовый класс может иметь виртуальную функцию color (), которая возвращает " NONE" по умолчанию. Функция Banana class color() возвращает "желтый" или "коричневый".

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

Я не хочу, чтобы кто-то мог кинуть error_base type, но типы исключений 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() метод чисто виртуальным. Он может иметь некоторое тело, но в случае отсутствия реализации по умолчанию, pure virtual является идеальной " реализацией "(="должен быть предоставлен классом потомка").

а теперь представьте себе то же самое, только не для печати, а для уничтожения:

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

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


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


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

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


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