РЕДАКТИРОВАТЬ: на основе последнего обновления вопроса и чата
Вот самое компактное ведение виртуального во всех ваших классах.
#include <iostream>
#include <vector>
using namespace std;
struct BaseFields {
int entityId{};
int16_t componentId{};
int8_t typeId{};
int16_t hpIdx;
int16_t flyPowerIdx;
};
vector<int> hp; // this will contain all the hit points, dynamically resizable, logic up to you
vector<float> flyPower; // this will contain all the fly powers, dynamically resizable, logic up to you
class BaseComponent {
public: // or protected
BaseFields data;
};
class HpOO : public virtual BaseComponent {
public:
void damage() {
hp[data.hpIdx] -= 1;
}
};
class FlyableOO : public virtual BaseComponent {
public:
void addFlyPower(float power) {
flyPower[data.hpIdx] += power;
}
};
class BirdOO : public virtual HpOO, public virtual FlyableOO {
public:
void suicidalFly() {
damage();
addFlyPower(5);
}
};
int main (){
std::cout<<"Base="<<sizeof(BaseComponent)<<std::endl; // 12
std::cout<<"C="<<sizeof(HpOO)<<std::endl; // 24
std::cout<<"D="<<sizeof(FlyableOO)<<std::endl; // 24
std::cout<<"E="<<sizeof(BirdOO)<<std::endl; // 32
}
версия меньшего размера класса, отбрасывающая все виртуальные вещи класса:
#include <iostream>
#include <vector>
using namespace std;
struct BaseFields {
};
vector<int> hp; // this will contain all the hit points, dynamically resizable, logic up to you
vector<float> flyPower; // this will contain all the fly powers, dynamically resizable, logic up to you
class BaseComponent {
public: // or protected
int entityId{};
int16_t componentId{};
int8_t typeId{};
int16_t hpIdx;
int16_t flyPowerIdx;
protected:
void damage() {
hp[hpIdx] -= 1;
};
void addFlyPower(float power) {
flyPower[hpIdx] += power;
}
void suicidalFly() {
damage();
addFlyPower(5);
};
};
class HpOO : public BaseComponent {
public:
using BaseComponent::damage;
};
class FlyableOO : public BaseComponent {
public:
using BaseComponent::addFlyPower;
};
class BirdOO : public BaseComponent {
public:
using BaseComponent::damage;
using BaseComponent::addFlyPower;
using BaseComponent::suicidalFly;
};
int main (){
std::cout<<"Base="<<sizeof(BaseComponent)<<std::endl; // 12
std::cout<<"C="<<sizeof(HpOO)<<std::endl; // 12
std::cout<<"D="<<sizeof(FlyableOO)<<std::endl; // 12
std::cout<<"E="<<sizeof(BirdOO)<<std::endl; // 12
// accessing example
constexpr int8_t BirdTypeId = 5;
BaseComponent x;
if( x.typeId == BirdTypeId ) {
auto y = reinterpret_cast<BirdOO *>(&x);
y->suicidalFly();
}
}
в этом примере предполагается, что ваши производные классы не имеют перекрывающихся функциональных возможностей с расходящимися эффектами, если у вас есть те, у вас есть необходимость добавить виртуальные функции в базовый класс для дополнительных служебных данных в 12 байтов (или 8, если вы упаковываете класс).
и, возможно, самая маленькая версия, поддерживающая виртуалы
#include <iostream>
#include <vector>
using namespace std;
struct BaseFields {
int entityId{};
int16_t componentId{};
int8_t typeId{};
int16_t hpIdx;
int16_t flyPowerIdx;
};
#define PACKED [[gnu::packed]]
vector<int> hp; // this will contain all the hit points, dynamically resizable, logic up to you
vector<float> flyPower; // this will contain all the fly powers, dynamically resizable, logic up to you
vector<BaseFields> baseFields;
class PACKED BaseComponent {
public: // or protected
int16_t baseFieldIdx{};
};
class PACKED HpOO : public virtual BaseComponent {
public:
void damage() {
hp[baseFields[baseFieldIdx].hpIdx] -= 1;
}
};
class PACKED FlyableOO : public virtual BaseComponent {
public:
void addFlyPower(float power) {
flyPower[baseFields[baseFieldIdx].hpIdx] += power;
}
};
class PACKED BirdOO : public virtual HpOO, public virtual FlyableOO {
public:
void suicidalFly() {
damage();
addFlyPower(5);
}
};
int main (){
std::cout<<"Base="<<sizeof(BaseComponent)<<std::endl; // 2
std::cout<<"C="<<sizeof(HpOO)<<std::endl; // 16 or 10
std::cout<<"D="<<sizeof(FlyableOO)<<std::endl; // 16 or 10
std::cout<<"E="<<sizeof(BirdOO)<<std::endl; // 24 or 18
}
первое число для неупакованной структуры, второе упакованное
Вы также можете упаковать hpIdx и flyPowerIdx в entityId с помощью трюка объединения:
union {
int32_t entityId{};
struct {
int16_t hpIdx;
int16_t flyPowerIdx;
};
};
в приведенном выше примере, если не использовать упаковку и переместить всю структуру BaseFields
в класс BaseComponent
, размеры остаются прежними.
END EDIT
Виртуальное наследование просто добавляет один размер указателя к классу плюс выравнивание указателя (при необходимости). Вы не можете обойти это, если вам действительно нужен виртуальный класс.
Вопрос, который вы должны себе задать, заключается в том, действительно ли вам это нужно. В зависимости от ваших методов доступа к этим данным это может быть не так.
Учитывая, что вам нужно виртуальное наследование, но все распространенные методы, которые должны вызываться из всех ваших классов, вы можете иметь виртуальный базовый класс и использовать немного меньше места, чем ваш первоначальный дизайн, следующим образом:
class Base{
public: int id=0;
virtual ~Base();
// virtual void Function();
};
class B : public Base{
public: int fieldB=0;
// void Function() override;
};
class C : public B{
public: int fieldC=0;
};
class D : public B{
public: int fieldD=0;
};
class E : public C, public D{
};
int main (){
std::cout<<"Base="<<sizeof(Base)<<std::endl; //16
std::cout<<"B="<<sizeof(B)<<std::endl; // 16
std::cout<<"C="<<sizeof(C)<<std::endl; // 24
std::cout<<"D="<<sizeof(D)<<std::endl; // 24
std::cout<<"E="<<sizeof(E)<<std::endl; // 48
}
В случае, если есть ошибки в кеше, но процессор все еще может обрабатывать результаты, вы можете уменьшить размер, используя специфичные для компилятора инструкции, чтобы сделать структуру данных как можно меньше (следующий пример работает в gcc):
#include<iostream>
class [[gnu::packed]] Base {
public:
int id=0;
virtual ~Base();
virtual void bFunction() { /* do nothing */ };
virtual void cFunction() { /* do nothing */ }
};
class [[gnu::packed]] B : public Base{
public: int fieldB=0;
void bFunction() override { /* implementation */ }
};
class [[gnu::packed]] C : public B{
public: int fieldC=0;
void cFunction() override { /* implementation */ }
};
class [[gnu::packed]] D : public B{
public: int fieldD=0;
};
class [[gnu::packed]] E : public C, public D{
};
int main (){
std::cout<<"Base="<<sizeof(Base)<<std::endl; // 12
std::cout<<"B="<<sizeof(B)<<std::endl; // 16
std::cout<<"C="<<sizeof(C)<<std::endl; // 20
std::cout<<"D="<<sizeof(D)<<std::endl; // 20
std::cout<<"E="<<sizeof(E)<<std::endl; //40
}
экономия дополнительных 8 байт по цене, возможно, некоторой загрузки ЦП (но если проблема с памятью, это может помочь).
Кроме того, если для каждого из ваших классов действительно есть одна функция, которую вы вызываете, вы должны иметь только одну функцию, которую вы переопределяете при необходимости.
#include<iostream>
class [[gnu::packed]] Base {
public:
virtual ~Base();
virtual void specificFunction() { /* implementation for Base class */ };
int id=0;
};
class [[gnu::packed]] B : public Base{
public:
void specificFunction() override { /* implementation for B class */ }
int fieldB=0;
};
class [[gnu::packed]] C : public B{
public:
void specificFunction() override { /* implementation for C class */ }
int fieldC=0;
};
class [[gnu::packed]] D : public B{
public:
void specificFunction() override { /* implementation for D class */ }
int fieldD=0;
};
class [[gnu::packed]] E : public C, public D{
void specificFunction() override {
// implementation for E class, example:
C::specificFunction();
D::specificFunction();
}
};
Это также позволит вам избежать необходимости выяснять, к какому классу относится этот объект, прежде чем вызывать соответствующую функцию.
Кроме того, если исходная идея наследования виртуальных классов лучше всего подходит для вашего приложения, вы можете реструктурировать свои данные, чтобы сделать их более доступными для целей кэширования, одновременно уменьшая размер ваших классов и одновременно обеспечивая доступность ваших функций. :
#include <iostream>
#include <array>
using namespace std;
struct BaseFields {
int id{0};
};
struct BFields {
int fieldB;
};
struct CFields {
int fieldB;
};
struct DFields {
int fieldB;
};
array<BaseFields, 1024> baseData;
array<BaseFields, 1024> bData;
array<BaseFields, 1024> cData;
array<BaseFields, 1024> dData;
struct indexes {
uint16_t baseIndex; // index where data for Base class is stored in baseData array
uint16_t bIndex; // index where data for B class is stored in bData array
uint16_t cIndex;
uint16_t dIndex;
};
class Base{
indexes data;
};
class B : public virtual Base{
public: void bFunction(){
//do something about "fieldB"
}
};
class C : public virtual B{
public: void cFunction(){
//do something about "fieldC"
}
};
class D : public virtual B{
};
class E : public virtual C, public virtual D{};
int main (){
std::cout<<"Base="<<sizeof(Base)<<std::endl; // 8
std::cout<<"B="<<sizeof(B)<<std::endl; // 16
std::cout<<"C="<<sizeof(C)<<std::endl; // 16
std::cout<<"D="<<sizeof(D)<<std::endl; // 16
std::cout<<"E="<<sizeof(E)<<std::endl; // 24
}
Очевидно, что это всего лишь пример, и предполагается, что у вас не более 1024 объектов в данный момент, вы можете увеличить это число, но выше 65536 вам придется использовать большее значение int для их хранения, также ниже 256 вы. можно использовать uint8_t для хранения индексов.
Кроме того, если одна из вышеприведенных структур добавляет очень мало накладных расходов к своему родительскому элементу, вы можете уменьшить количество массивов, которые вы используете для хранения данных, если есть очень небольшая разница в размере объектов, вы можете просто сохранить все данные в единая структура и более локализованный доступ к памяти. Все зависит от вашего приложения, поэтому я не могу дать здесь больше советов, кроме оценки того, что лучше всего подходит для вашего случая.
Получайте удовольствие и наслаждайтесь C ++.