Навіщо нам потрібен чистий віртуальний деструктор на C ++?


154

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

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

Тож мої запитання є

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

  2. Коли ми створюємо абстрактні класи, чи є гарною практикою зробити деструктор також чистим віртуальним? Якщо так .. то чому?



14
@ Daniel- Згадані посилання не відповідають на моє запитання. Він відповідає, чому у чистого віртуального деструктора має бути визначення. Моє запитання, чому нам потрібен чистий віртуальний деструктор.
Марк

Я намагався з’ясувати причину, але ви вже задали тут питання.
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 ++ Lite Lite говорить: "Зауважте, що можна надати визначення чистої віртуальної функції, але це зазвичай бентежить новачків і найкраще уникати їх пізніше". parashift.com/c++-faq-lite/abcs.html#faq-22.4 Вікіпедія (цей бастіон правильності) також говорить так само. Я вважаю, що стандарт ISO / IEC використовує подібну термінологію (на жаль, на даний момент моя копія працює) ... Я згоден, що це заплутано, і я, як правило, не використовую цей термін без уточнення, коли я надаю визначення, особливо навколо новіших програмістів ...
орендувач

9
@Motti: Що тут цікаво і створює більше плутанини, це те, що чистого віртуального деструктора НЕ потрібно чітко переосмислювати у похідному (та інстанційному) класі. У такому випадку використовується неявне визначення :)
kappa

33

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


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

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

19

Якщо ви хочете створити абстрактний базовий клас:

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

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

Для нашого гіпотетичного ABC:

Ви гарантуєте, що він не може бути ініційований (навіть внутрішній для самого класу, тому приватних конструкторів може бути недостатньо), ви отримуєте потрібну віртуальну поведінку для деструктора, і вам не доведеться знаходити та мітити інший метод, який не відповідає Вам не потрібна віртуальна відправка як "віртуальна".


8

З відповідей, які я прочитав на ваше запитання, я не міг зрозуміти вагомих причин фактично використовувати чистий віртуальний деструктор. Наприклад, наступна причина мене зовсім не переконує:

Можливо, справжньою причиною того, що дозволені чисті віртуальні деструктори, є те, що забороняти їх означатиме додавання до мови іншого правила, і в цьому правилі немає необхідності, оскільки жодних побічних ефектів не може виникнути, якщо дозволити чистий віртуальний деструктор.

На мою думку, чисті віртуальні деструктори можуть бути корисними. Наприклад, припустимо, що у вашому коді є два класи myClassA та myClassB, і що myClassB успадковується від myClassA. З причин, про які говорив Скотт Майєрс у своїй книзі "Ефективніший C ++", пункт 33 "Зробити нелістові класи абстрактними", краще практично створити абстрактний клас myAbpositeClass, з якого успадковують myClassA та myClassB. Це забезпечує кращу абстракцію та запобігає появі деяких проблем, які виникають, наприклад, з копіями об'єктів.

У процесі абстрагування (створення класу myAb абстрактClass) може бути так, що жоден метод myClassA або myClassB не є хорошим кандидатом на те, щоб бути чистим віртуальним методом (що є необхідною умовою для того, щоб myAbpositeClass був абстрактним). У цьому випадку ви визначаєте деструктор абстрактного класу чистим віртуальним.

Надалі конкретний приклад із якогось коду я написав сам. У мене є два класи - числові / фізичні парами, які мають загальні властивості. Тому я дозволяю їм успадкувати абстрактний клас 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захищеним, як було зазначено в іншому коментарі.
rwols

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

    База * pBase = новий Похідний (); видалити pBase; чистий віртуальний деструктор не потрібно, лише віртуальний деструктор зробить цю роботу.


2

Ви натрапляєте на гіпотетику з цими відповідями, тому я спробую зробити більш зрозуміле і зрозуміле пояснення задля ясності.

Основні співвідношення об'єктно-орієнтованого дизайну два: IS-A і HAS-A. Я цього не придумував. Саме так їх називають.

IS-A вказує на те, що конкретний об'єкт ідентифікує як клас класу, який знаходиться над ним у ієрархії класів. Банановий об’єкт - це фруктовий об’єкт, якщо він є підкласом фруктового класу. Це означає, що де завгодно фруктовий клас можна використовувати банан. Однак це не рефлексивно. Ви не можете замінити базовий клас для конкретного класу, якщо для цього вимагається певний клас.

Has-вказав, що об’єкт є частиною складеного класу і що існує відносини власності. Це означає в C ++, що він є об'єктом-членом, і, як такий, onus належить класу володіння, щоб розпоряджатися ним або передавати право власності, перш ніж знищити себе.

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

Віртуальні функції - це, по-перше, річ, що виконується. Він є частиною поліморфізму в тому, що він використовується для того, щоб визначити, яку функцію виконувати в той момент, коли вона викликається в запущеній програмі.

Віртуальне ключове слово - це директива компілятора для прив’язки функцій у певному порядку, якщо є неоднозначність щодо ідентичності класу. Віртуальні функції завжди є в батьківських класах (наскільки я знаю) і вказують компілятору, що прив'язка функцій-членів до їх імен повинна відбуватися спочатку за допомогою функції підкласу, а після цього функція батьківського класу.

Клас Fruit може мати віртуальний колір функції (), який повертає "NONE" за замовчуванням. Функція кольору () класу Banana повертає "ЖОВТИЙ" або "РІЗНИЙ".

Але якщо функція, що приймає вказівник Fruit, викликає color () для надісланого йому класу Banana - який колір () функція буде викликаний? Функція зазвичай називає 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), функції стають легшими та універсальнішими з менш зайвим кодом. Вам більше не потрібно писати функції для кожного виду фруктів, і ви знаєте, що кожен фрукт буде відповідати кольору () своєю правильною функцією.

Я сподіваюся, що це завзято пояснення зміцнює концепцію, а не плутає речі. Є багато хороших прикладів для того, щоб подивитися і подивитися достатньо і насправді запустити їх і возитися з ними, і ви отримаєте це.


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

Клас izjem_string, який зберігається приватним, приховує рядок std :: у моєму публічному інтерфейсі:

// 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 ++, третє лише намагання робити речі з нульовими вказівниками та перевищенням меж масивів. Невиртуальний деструктор базового класу буде викликаний загальним покажчиком, повністю обходячи деструктор підкласу, якщо він не позначений віртуальним. Якщо є якісь динамічно створені об'єкти, що належать до підкласу, вони не будуть відновлені базовим деструктором під час виклику видалення. Ви цураєтесь чудово, то BLUURRK! (важко знайти, де теж.)
Кріс Рейд,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.