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


154

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

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

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

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

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


4
Несколько дубликатов: stackoverflow.com/questions/999340/… и stackoverflow.com/questions/630950/pure-virtual-destructor-in-c являются двумя из них
Дэниел Слооф

14
@ Daniel- Упомянутые ссылки не отвечают на мой вопрос. Он отвечает, почему у чистого виртуального деструктора должно быть определение. Мой вопрос: зачем нам нужен чистый виртуальный деструктор?
Mark

Я пытался выяснить причину, но вы уже задали вопрос здесь.
nsivakr

Ответы:


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

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

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

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

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

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

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

13
«Да, у чисто виртуальных функций могут быть реализации» Тогда это не чисто виртуально.
GManNickG

2
Если вы хотите сделать класс абстрактным, не проще ли сделать все конструкторы защищенными?
bdonlan

78
@GMan, вы ошибаетесь, потому что быть чисто виртуальным означает, что производные классы должны переопределять этот метод, это ортогонально реализации. Проверьте мой код и закомментируйте, foof::barесли хотите сами убедиться.
Мотти

15
@GMan: C ++ FAQ Lite говорит: «Обратите внимание, что можно предоставить определение для чисто виртуальной функции, но обычно это сбивает с толку новичков, и лучше избегать этого позже». parashift.com/c++-faq-lite/abcs.html#faq-22.4 Википедия (этот бастион правильности) также говорит о том же. Я полагаю, что в стандарте ИСО / МЭК используется аналогичная терминология (к сожалению, моя копия в данный момент работает) ... Я согласен, что это сбивает с толку, и я обычно не использую этот термин без пояснений, когда даю определение, особенно вокруг новых программистов ...
Leander

9
@Motti: Что здесь интересно и дает больше путаницы, так это то, что чистый виртуальный деструктор НЕ нуждается в явном переопределении в производном (и инстанцированном) классе. В таком случае используется неявное определение :)
kappa

33

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


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

4
@Surfing: потому что деструктор производного класса неявно вызывает деструктор своего базового класса, даже если этот деструктор является чисто виртуальным. Так что, если нет реализации для него, произойдет неопределенное поведение.
a.peganz

19

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

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

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

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

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


8

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

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

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

1
Мне нравится это использование, но другой способ «обеспечить» наследование - это объявить конструктор IParamдля защиты, как было отмечено в некоторых других комментариях.
Рволс

4

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


3

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

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

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

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


2

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

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

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

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

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

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

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

Класс Fruit может иметь виртуальную функцию color (), которая по умолчанию возвращает «NONE». Функция класса Banana color () возвращает «ЖЕЛТЫЙ» или «КОРИЧНЕВЫЙ».

Но если функция, принимающая указатель 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 () своей собственной правильной функцией.

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


1

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


0

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

Я не хочу , чтобы кто - нибудь в состоянии бросить 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>&) {
}

0

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

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

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

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

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


-2

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

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


-2

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


-1: Вопрос не в том, почему деструктор должен быть виртуальным.
Трубадур

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

Вы на 100% правы. В прошлом это было и было одним из источников утечек и сбоев номер один в программах на C ++, третье - только попытка сделать что-то с нулевыми указателями и выход за пределы массивов. Не виртуальный деструктор базового класса будет вызываться по универсальному указателю, полностью обходя деструктор подкласса, если он не помечен как виртуальный. Если есть какие-либо динамически созданные объекты, принадлежащие подклассу, они не будут восстановлены базовым деструктором при вызове delete. Вы хорошо пыхтите тогда, BLUURRK! (Трудно найти, где тоже.)
Крис Рейд
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.