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

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

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

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

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

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


person Mark    schedule 02.08.2009    source источник
comment
Множественные дубликаты: stackoverflow.com/questions/999340/ и stackoverflow.com/questions/630950/pure- virtual-destructor-in-c - двое из них   -  person Daniel Sloof    schedule 02.08.2009
comment
@ Daniel - указанные ссылки не отвечают на мой вопрос. Он отвечает, почему чистый виртуальный деструктор должен иметь определение. Мой вопрос в том, зачем нам нужен чистый виртуальный деструктор.   -  person Mark    schedule 02.08.2009
comment
Я пытался выяснить причину, но вы уже задавали вопрос здесь.   -  person nsivakr    schedule 25.08.2010


Ответы (12)


  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.
};
person Motti    schedule 02.08.2009
comment
да, чистые виртуальные функции могут иметь реализации. Тогда это не чисто виртуальные функции. - person GManNickG; 02.08.2009
comment
Если вы хотите сделать класс абстрактным, не было бы проще просто защитить все конструкторы? - person bdonlan; 02.08.2009
comment
@GMan, вы ошибаетесь, поскольку чисто виртуальные классы должны переопределять этот метод, это ортогонально реализации. Посмотрите мой код и прокомментируйте foof::bar, если хотите убедиться в этом сами. - person Motti; 02.08.2009
comment
@GMan: в C ++ FAQ lite говорится, что можно дать определение чистой виртуальной функции, но это обычно сбивает с толку новичков, и этого лучше избегать на потом. parashift.com/c++-faq-lite/abcs.html# faq-22.4 Википедия (этот оплот правильности) также говорит то же самое. Я считаю, что в стандарте ISO / IEC используется аналогичная терминология (к сожалению, моя копия сейчас работает) ... Я согласен, что это сбивает с толку, и я обычно не использую этот термин без пояснений, когда даю определение, особенно вокруг новых программистов ... - person leander; 03.08.2009
comment
@Motti: Что здесь интересно и вызывает большую путаницу, так это то, что чистый виртуальный деструктор НЕ нужно явно переопределять в производном (и созданном) классе. В таком случае используется неявное определение :) - person kappa; 28.08.2014
comment
Когда вы говорите, что класс, в котором он определен, будет полезным, почему бы вам просто не сказать, что вам нужно поставить реализацию для создания экземпляра любого производного класса? Это делает его более простым - person meneldal; 29.05.2015
comment
стоит упомянуть возможность использовать =default для чистого виртуального деструктора, как упоминалось здесь - person Hanna Khalil; 14.12.2016
comment
@HannaKhalil, правда, но A) Я написал это в 2009 году, еще до того, как = default стал популярным. Б) Я действительно не вижу преимущества = default перед {} для деструкторов (за исключением лишнего набора текста). - person Motti; 14.12.2016
comment
@Motti Я не виню вас за то, что вы не добавили его в то время. Я говорю, что теперь, после релиза cpp11, его можно было добавить. Что касается второго пункта, вы правы, но было бы неплохо добавить его, потому что это может сбивать с толку наличие 2 '= something' для одного и того же метода. Во всяком случае, ваше решение - person Hanna Khalil; 14.12.2016

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

person Braden    schedule 02.08.2009
comment
но все же зачем предоставлять реализацию чистого виртуального деструктора. Что может пойти не так? Я делаю деструктор чисто виртуальным и не предоставляю его реализацию. Я предполагаю, что объявлены только указатели базовых классов, и поэтому деструктор для абстрактного класса никогда не вызывается. - person Krishna Oza; 10.03.2014
comment
@Surfing: потому что деструктор производного класса неявно вызывает деструктор своего базового класса, даже если этот деструктор является чисто виртуальным. Так что, если для этого нет реализации, произойдет неопределенное поведение. - person a.peganz; 02.09.2014

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

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

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

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

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

person leander    schedule 02.08.2009
comment
Сравнивая ответы по баллам, это первый ответ, который: 1) правильный, 2) написан синтетическим тоном (в отличие от использования примеров и obiter dictum), 3) ответ на вопрос, как он написан в title, а 4) показывает довольно распространенный вариант использования (например, «чистые структуры» с переменными размерами и без методов). Престижность + голос - person DomQ; 08.04.2021

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

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

На мой взгляд, чистые виртуальные деструкторы могут быть полезны. Например, предположим, что у вас есть два класса 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; 
};
person Laurent Michel    schedule 11.01.2015
comment
Мне нравится это использование, но еще один способ принудительного наследования - объявить конструктор IParam защищенным, как было отмечено в другом комментарии. - person rwols; 29.05.2015

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

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 = new Derived (); удалить pBase; чистый виртуальный деструктор не требуется, только виртуальный деструктор выполнит эту работу.

person Anil8753    schedule 10.09.2014

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

person sukumar    schedule 21.04.2011

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

Основных отношений объектно-ориентированного проектирования два: 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 (), если все, что он знает, - это то, что это Я-указатель на 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 () своей собственной правильной функцией.

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

person Chris Reid    schedule 11.04.2017

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

person J-Q    schedule 02.11.2017

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

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

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

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

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

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

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

person Jarek C    schedule 30.06.2017

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

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

person Steven Sudit    schedule 02.08.2009

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

person Asad hashmi    schedule 09.09.2013
comment
-1: вопрос не в том, почему деструктор должен быть виртуальным. - person Troubadour; 09.09.2013
comment
Более того, в определенных ситуациях деструкторы не обязательно должны быть виртуальными, чтобы обеспечить правильное разрушение. Виртуальные деструкторы необходимы только тогда, когда вы в конечном итоге вызываете delete для указателя на базовый класс, когда на самом деле он указывает на его производный. - person CygnusX1; 31.10.2013
comment
Вы на 100% правы. Это и было в прошлом одним из основных источников утечек и сбоев в программах на C ++, третьим только после попыток делать что-то с нулевыми указателями и выходом за границы массивов. Деструктор невиртуального базового класса будет вызываться для универсального указателя, полностью обходя деструктор подкласса, если он не помечен как виртуальный. Если есть какие-либо динамически созданные объекты, принадлежащие подклассу, они не будут восстановлены базовым деструктором при вызове удаления. Ну ладно, тогда BLUURRK! (где тоже трудно найти.) - person Chris Reid; 11.04.2017