В настоящее время я столкнулся со следующей ситуацией:
У меня есть базовый класс 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
?
Вы можете создать другую иерархию: BaseData
, AData
, BData
, CData
и сделать V
BaseData*
(или еще лучше — умный указатель на BaseData
).
Лучше всего отказаться от фразы «Я не хочу, чтобы пользователь вручную освобождал V в деструкторе каждого производного класса», потому что все остальное, что вы можете придумать, - это просто обходной путь, который сводится к одному и тому же.
Похоже на проблему XY. Зачем базовому классу нужен этот указатель? Почему вы не следуете правилу нуля? В идеале у производных классов есть только члены, и эти члены заботятся о своих собственных ресурсах. Никуда писать delete
не нужно.
void*
— это идиома языка C, обозначающая любые данные. Современный C++ имеет std::any для управления данными любого типа. Вопрос в том, зачем классу Base
знать об этих данных? Разве простые члены производных классов не подойдут? Возможно, рассмотрите шаблон для производных классов с конкретными членами, например. Specific<T>
, содержащий T member
.
Похоже, вам просто нужно использовать std::variant
из C++17.
Если я вас правильно понимаю, у вас есть иерархия сущностей, производные из которых (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>(); }
};
Примечания:
std::array
вместо необработанного массива C.structs
предназначены только для простоты.Base
принятия std::unique_ptr<BaseData>
, чтобы убедиться, что производные классы инициализируются m_V
. m_V
также можно сделать приватным, разрешив доступ к нему через метод доступа.Если вы добавите конструктор в класс Base
с параметром std::unique_ptr<BaseData>
(переместив его в член), производные классы не смогут забыть установить m_V
. Если хотите, вы можете сделать m_V
приватным, возможно, предложив функцию-получатель, возвращающую BaseData const&
вместо unique_ptr
.
@Jens Я хотел сделать базовый пример максимально простым, но добавил примечание, в котором упоминаются предложенные вами улучшения.
Есть много способов решить вашу проблему, какой из них лучше всего зависит от ваших требований, о которых я не знаю.
И еще одно: вы используете наследование для совместного использования реализации. Обычно это не лучший подход; используйте наследование для совместного использования интерфейсов и композицию для совместного использования реализации. Я бы предложил решить вашу проблему, создав класс хранения данных, который 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
Вы намеренно «отключили» систему типов с помощью void, поэтому она вам мало чем поможет. У вас может быть член указателя функции, который выполняет освобождение.