Зачем нам нужен чистый виртуальный деструктор в C ++? - PullRequest
144 голосов
/ 02 августа 2009

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

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

Так что мои вопросы

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

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

Ответы [ 12 ]

109 голосов
/ 02 августа 2009
  1. Вероятно, настоящая причина, по которой разрешены чистые виртуальные деструкторы, заключается в том, что запретить их будет означать добавление еще одного правила к языку, и в этом правиле нет необходимости, поскольку от использования чистого виртуального деструктора не может быть никаких побочных эффектов.

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

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

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

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

18 голосов
/ 03 августа 2009

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

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

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

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

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

7 голосов
/ 11 января 2015

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

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

На мой взгляд, чистые виртуальные деструкторы могут быть полезны. Например, предположим, что в вашем коде есть два класса 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; 
};
4 голосов
/ 21 апреля 2011

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

2 голосов
/ 11 апреля 2017

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

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

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

Имеет-указывает, что объект является частью составного класса и что существуют отношения собственности. В C ++ это означает, что это объект-член, и поэтому ответственность за его уничтожение или передачу права собственности перед уничтожением себя лежит на классе-владельце.

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

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

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

Класс 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 () своей собственной правильной функцией.

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

2 голосов
/ 10 сентября 2014

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

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

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

0 голосов
/ 02 ноября 2017

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

0 голосов
/ 30 июня 2017

Может быть, есть еще РЕАЛЬНОЕ ИСПОЛЬЗОВАНИЕ чистого виртуального деструктора, которого я не вижу в других ответах:)

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

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

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

Это упрощенный вариант использования из моего реального приложения. Единственная разница здесь в том, что вместо «нормального» * ​​1023 * был использован «специальный» метод (деструктор). Но причина, по которой он является чисто виртуальным, остается той же - нет кода по умолчанию для метода. Немного сбивает с толку тот факт, что ДОЛЖЕН быть эффективный деструктор, а компилятор фактически генерирует для него пустой код. Но с точки зрения программиста чистая виртуальность по-прежнему означает: «У меня нет кода по умолчанию, он должен предоставляться производными классами».

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

0 голосов
/ 16 мая 2014

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

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