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

В настоящее время я столкнулся со следующей ситуацией:

У меня есть базовый класс Base с членом void* V, на самом деле это может быть A*, B*, C*, и три (только три, фиксированное число) категории класса будут производными от Base, каждая будет заполнять V разными типами данных. Я не хочу, чтобы пользователь вручную освобождал V в деструкторе каждого производного класса, а затем Base должен определить реальный тип V и вызвать его деструктор. Но поскольку виртуальные функции считаются невиртуальными, как мне получить тип V? он будет занимать больше памяти, если я дополнительно добавлю переменную _type (и это заставит пользователя заполнять _type для каждого конструктора.

Пример здесь:

class Base {
public:
   void* V;
   virtual ~Base() { /* how to free V? */ }
};

class A : public Base {
public:
   A() : V((void*) new int) {}
   virtual ~A() { /* I don't want to let user 
                     write delete (int*) V in every destructor of 
                     class of type 1, since the user may forget. */
   virtual int type() const { return 1; }
};

class B : public Base {
public:
   B() : V((void*) new double) {}
   
   virtual int type() const { return 2; }
};

class C : public Base {
public:
   C() : V((void*) new int[4]) {}
   
   virtual int type() const { return 3; }
};


Моя главная цель — делать больше вещей для пользователей и не позволять им вручную управлять меморией. Есть ли в этой ситуации лучший способ достичь той же цели? Или есть обходной путь, чтобы правильно освободить V?

Вы намеренно «отключили» систему типов с помощью void, поэтому она вам мало чем поможет. У вас может быть член указателя функции, который выполняет освобождение.

Quimby 30.08.2024 08:06

Вы можете создать другую иерархию: BaseData, AData, BData, CData и сделать VBaseData* (или еще лучше — умный указатель на BaseData).

wohlstad 30.08.2024 08:13

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

j6t 30.08.2024 08:17

Похоже на проблему XY. Зачем базовому классу нужен этот указатель? Почему вы не следуете правилу нуля? В идеале у производных классов есть только члены, и эти члены заботятся о своих собственных ресурсах. Никуда писать delete не нужно.

Sebastian Redl 30.08.2024 09:24
void* — это идиома языка C, обозначающая любые данные. Современный C++ имеет std::any для управления данными любого типа. Вопрос в том, зачем классу Base знать об этих данных? Разве простые члены производных классов не подойдут? Возможно, рассмотрите шаблон для производных классов с конкретными членами, например. Specific<T>, содержащий T member.
Jens 30.08.2024 09:57

Похоже, вам просто нужно использовать std::variant из C++17.

Marek R 30.08.2024 12:28
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
110
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Если я вас правильно понимаю, у вас есть иерархия сущностей, производные из которых (A, B и т. д.) содержат разные типы данных.

Самым простым решением было бы удалить V из Base и добавить определенный элемент данных (без указателя) в каждый производный класс. Таким образом, вам вообще не придется иметь дело с удалением V.

Если по какой-то причине вы хотите отделить основные объекты от данных, которые они хранят (чтобы база имела к ним доступ), вы можете создать отдельную иерархию объектов данных (BaseData, AData, BData и т. д.). Вы также можете использовать интеллектуальный указатель, чтобы избежать ручного управления памятью.

Это показано ниже:

#include <memory>
#include <array>

// Hirarchy of data entities:

struct BaseData { 
    virtual ~BaseData() = default; // required for proper destruction of derived classes
};

struct AData : public BaseData {
    int m_data{ 0 };
};

struct BData : public BaseData {
    double m_data{ 0 };
};

struct CData : public BaseData {
    std::array<int, 4> m_data{ 0 };
};


// Hirarchy of main entities:

class Base {
public:
    std::unique_ptr<BaseData> m_V;
    virtual ~Base() = default; // required for proper destruction of derived classes
};

class A : public Base {
public:
    A() { m_V = std::make_unique<AData>(); }
};

class B : public Base {
public:
    B() { m_V = std::make_unique<BData>(); }
};

class C : public Base {
public:
    C() { m_V = std::make_unique<CData>(); }
};

Примечания:

  1. Я использовал std::array вместо необработанного массива C.
  2. Объекты данных structs предназначены только для простоты.
  3. Как упоминал @Jens в комментарии, вы можете добавить конструктор для Base принятия std::unique_ptr<BaseData>, чтобы убедиться, что производные классы инициализируются m_V. m_V также можно сделать приватным, разрешив доступ к нему через метод доступа.

Если вы добавите конструктор в класс Base с параметром std::unique_ptr<BaseData> (переместив его в член), производные классы не смогут забыть установить m_V. Если хотите, вы можете сделать m_V приватным, возможно, предложив функцию-получатель, возвращающую BaseData const& вместо unique_ptr.

Jens 30.08.2024 09:49

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

wohlstad 30.08.2024 10:53

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

И еще одно: вы используете наследование для совместного использования реализации. Обычно это не лучший подход; используйте наследование для совместного использования интерфейсов и композицию для совместного использования реализации. Я бы предложил решить вашу проблему, создав класс хранения данных, который A, B и C могут хранить в качестве переменной-члена.

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

Шаблоны

Если вам не нужно, чтобы Base был полезен сам по себе, вы можете просто сделать его шаблонным:

template<typename DataType>
class Base {
public:
    DataType V{};
    virtual ~Base() = default;
};
class A : public Base<int> {};

Умные указатели

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

class Base {
public:
    std::shared_ptr<void> V;
    virtual ~Base() = default;
};
class A : public Base {
public:
    A() : V{std::make_shared<int>()} {}
};

Переменная _type

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

Другую проблему, связанную с необходимостью установки _type, можно решить, просто добавив конструктор:

class Base {
public:
    void* V;
    Base(int* a) : V{a}, _type{Int} {}
    // ... constructors for other supported data types
    virtual ~Base() { switch (_type) { ... } }

private:
    enum { Int, Double, IntArray } _type;
};
class A : public Base {
public:
    A():Base{new int} {}
};

станд::вариант

Решение _type можно значительно улучшить, используя std::variant, который делает все это за вас и без выделения кучи:

class Base {
public:
    std::variant<int, double, std::array<int, 4>> V;
    virtual ~Base() = default;
};
class A : public Base {
public:
    A() :V{std::in_place_type<int>} {}
};

Я действительно думаю, что тебе стоит послушать @Wutz,

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

#include <iostream>
#include <cstring>
using std::cout;
using std::endl;

template<class T>
class Base {
public:
   char type;// explicit type, what you didn't want right?!
   T* V;
   Base(char c):type(c){};
   virtual ~Base() { 
       cout<<"Base desctruct : explicit obj type "<< type <<" _ type "<< typeid( this ).name()<< " V type is "<< typeid( V ).name()<< " *V type is "<< typeid( *V ).name() <<endl;
       if (V){/* in case user deleted it already */
        cout<<"\t _ value is "<<*V <<" ("<<V<<')'<<endl;
           delete [] V;
           delete V;
           V = 0;
           cout<<"\tDeleted V form Base"<<endl;
           }else cout<<"\tNothing to delete"<<endl;
        if (V){
           cout<<"V still exists"<<endl;
           }
    }
};

class A : public Base<char> {
public:
   A() : Base('A') { V = new char[12];strcpy(V,"AAA_AAA");}
   virtual ~A() {
   }
};
class B : public Base<double> {
public:   
    B() : Base('B') {V = new double(1.618);}
};

class C : public Base<int> {
public:   
   C(): Base('C')   {V = new int[4]{8,2,3,4};}
};

class D : public C {
public:   
   D(): C()   {type = 'D';}
      virtual ~D() {
          cout<<"Child desctruct : obj type "<< type <<endl;
        if (V){         
            delete [] V;
           delete V;
           V=0;
           cout<<"\tDeleted V from child"<<endl;
           }
   }
};

int main(){
    A *a = new A();
    delete a;
    B *b = new B();
    delete b;
    C *c = new C();
    delete c;
    A a2;
    D d;
    return 0;
}

он выводит

Base desctruct : explicit obj type A _ type P4BaseIcE V type is Pc *V type is c
         _ value is A (AAA_AAA)
        Deleted V form Base
Base desctruct : explicit obj type B _ type P4BaseIdE V type is Pd *V type is d
         _ value is 1.618 (0xe51908)
        Deleted V form Base
Base desctruct : explicit obj type C _ type P4BaseIiE V type is Pi *V type is i
         _ value is 8 (0xe519f8)
        Deleted V form Base
Child desctruct : obj type D
        Deleted V from child
Base desctruct : explicit obj type D _ type P4BaseIiE V type is Pi *V type is i
        Nothing to delete
Base desctruct : explicit obj type A _ type P4BaseIcE V type is Pc *V type is c
         _ value is A (AAA_AAA)
        Deleted V form Base

Другие вопросы по теме