Я только что проверил размер класса, содержащего десятки виртуальных методов, с помощью g++ (4.7), потому что я слышал, что указатели используются для виртуальных методов, и я подумал, что это будет ужасная реализация, так как она будет занимать 80 байт для каждого экземпляра класса. класс с всего 10 виртуальными методами в моей системе.
К моему облегчению, sizeof(<insert typename here>)
вернул только 8 байтов, размер указателя в моей системе. Я предполагаю, что это означает, что он хранит указатель на vtable, а не на каждый метод, и что я просто неправильно понял, что говорили люди (или, возможно, большинство компиляторов глупы).
Однако, прежде чем я, наконец, протестировал это, я боролся с использованием виртуальных методов в качестве указателей так, как я ожидал, что они будут работать. Я заметил, что адрес на самом деле был относительно небольшим числом, часто меньше 100 и с разницей в 8 байт по сравнению с другими, поэтому я предположил, что это какой-то индекс для массива. И тут я задумался о том, как бы мне самому реализовать vtables, и чтобы не было использования указателя, как ясно показывают результаты моего теста. Я был удивлен, увидев, что он использует целых 8 байтов (я проверил, было ли это просто заполнение, вставив поле char, которое вернуло 16 байтов с sizeof).
Вместо этого я бы реализовал это, сохранив индекс массива (например, 4 байта или даже 2, если используется 65536 или меньше классов с виртуальными методами), который будет искаться в таблице поиска, содержащей указатели на виртуальные таблицы, и найти это так. Итак, почему указатель сохраняется? Из соображений производительности, или они просто повторно использовали код для 32-битных операционных систем (поскольку это не имело бы никакого значения в размере памяти)?
Заранее спасибо.
изменить:
Кто-то попросил меня рассчитать фактическую сэкономленную память, и я решил сделать пример кода. К сожалению, он стал довольно большим (от меня требовали использовать 10 виртуальных методов в обоих), но я протестировал его, и он действительно работает. Вот оно:
#include <cstdio>
#include <cstdlib>
/* For the singleton lovers in this community */
class VirtualTableManager
{
unsigned capacity, count;
void*** vtables;
public:
~VirtualTableManager() {
delete vtables;
}
static VirtualTableManager& getInstance() {
static VirtualTableManager instance;
return instance;
}
unsigned addElement(void** vtable) {
if (count == capacity)
{
vtables = (void***) realloc(vtables, (capacity += 0x2000) * sizeof(void**)); /* Reserves an extra 64KiB of pointers */
}
vtables[count] = vtable;
return count++;
}
void** getElement(unsigned index) {
return index < capacity ? vtables[index] : 0; /* Just in case: "Hey guys, let's misuse the API!" */
}
private:
VirtualTableManager() : capacity(0), count(0), vtables(0) { }
VirtualTableManager(const VirtualTableManager&);
void operator =(const VirtualTableManager&);
};
class Real
{
public:
short someField; /* This is required to show the difference, because of padding */
Real() : someField(0) { }
virtual ~Real() {
printf("Real::~Real()\n");
}
virtual void method0() {
printf("Real::method0()\n");
}
virtual void method1(short argument) {
someField = argument;
}
virtual short method2() {
return someField;
}
virtual void method3() { }
virtual void method4() { }
virtual void method5() { }
virtual void method6() { }
virtual void method7() { }
virtual void method8() { }
};
class Fake
{
static void** vtable;
static unsigned classVIndex; /* Don't know what to call it, please forgive me for the lame identifier */
public:
unsigned instanceVIndex;
short someField;
Fake() : instanceVIndex(classVIndex), someField(0) { }
~Fake() {
reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[9])(this);
}
void method0() {
reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[0])(this);
}
void method1(short argument) {
reinterpret_cast<void (*)(Fake*, short argument)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[1])(this, argument);
}
short method2() {
return reinterpret_cast<short (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[2])(this);
}
void method3() {
reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[3])(this);
}
void method4() {
reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[4])(this);
}
void method5() {
reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[5])(this);
}
void method6() {
reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[6])(this);
}
void method7() {
reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[7])(this);
}
void method8() {
reinterpret_cast<void (*)(Fake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[8])(this);
}
protected:
Fake(unsigned instanceVIndex, short someField)
: instanceVIndex(instanceVIndex), someField(someField) { }
/* The 'this' keyword is an automatically passed pointer, so I'll just manually pass it and identify it as 'self' (thank you, lua, I would have used something like 'vthis', which would be boring and probably incorrect) */
static void vmethod0(Fake* self) {
printf("Fake::vmethod0(%p)\n", self);
}
static void vmethod1(Fake* self, short argument) {
self->someField = argument;
}
static short vmethod2(Fake* self) {
return self->someField;
}
static void vmethod3(Fake* self) { }
static void vmethod4(Fake* self) { }
static void vmethod5(Fake* self) { }
static void vmethod6(Fake* self) { }
static void vmethod7(Fake* self) { }
static void vmethod8(Fake* self) { }
static void vdestructor(Fake* self) {
printf("Fake::vdestructor(%p)\n", self);
}
};
class DerivedFake : public Fake
{
static void** vtable;
static unsigned classVIndex;
public:
DerivedFake() : Fake(classVIndex, 0) { }
~DerivedFake() {
reinterpret_cast<void (*)(DerivedFake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[1])(this);
}
void method0() {
reinterpret_cast<void (*)(DerivedFake*)>(VirtualTableManager::getInstance().getElement(instanceVIndex)[0])(this);
}
protected:
DerivedFake(unsigned instanceVIndex, short someField)
: Fake(instanceVIndex, someField) { }
static void vmethod0(DerivedFake* self) {
printf("DerivedFake::vmethod0(%p)\n", self);
}
static void vdestructor(DerivedFake* self) {
printf("DerivedFake::vdestructor(%p)\n", self);
Fake::vdestructor(self); /* call parent destructor */
}
};
/* Make the vtable */
void** Fake::vtable = (void*[]) {
(void*) &Fake::vmethod0, (void*) &Fake::vmethod1,
(void*) &Fake::vmethod2, (void*) &Fake::vmethod3,
(void*) &Fake::vmethod4, (void*) &Fake::vmethod5,
(void*) &Fake::vmethod6, (void*) &Fake::vmethod7,
(void*) &Fake::vmethod8, (void*) &Fake::vdestructor
};
/* Store the vtable and get the look-up index */
unsigned Fake::classVIndex = VirtualTableManager::getInstance().addElement(Fake::vtable);
/* Do the same for derived class */
void** DerivedFake::vtable = (void*[]) {
(void*) &DerivedFake::vmethod0, (void*) &Fake::vmethod1,
(void*) &Fake::vmethod2, (void*) &Fake::vmethod3,
(void*) &Fake::vmethod4, (void*) &Fake::vmethod5,
(void*) &Fake::vmethod6, (void*) &Fake::vmethod7,
(void*) &Fake::vmethod8, (void*) &DerivedFake::vdestructor
};
unsigned DerivedFake::classVIndex = VirtualTableManager::getInstance().addElement(DerivedFake::vtable);
int main_virtual(int argc, char** argv)
{
printf("size of 100 instances of Real including padding is %lu bytes\n"
"size of 100 instances of Fake including padding is %lu bytes\n",
sizeof(Real[100]), sizeof(Fake[100]));
Real *real = new Real;
Fake *fake = new Fake;
Fake *derived = new DerivedFake;
real->method1(123);
fake->method1(456);
derived->method1(789);
printf("real::method2() = %hi\n"
"fake::method2() = %hi\n"
"derived::method2() = %hi\n", real->method2(), fake->method2(), derived->method2());
real->method0();
fake->method0();
derived->method0();
delete real;
delete fake;
delete derived;
return 0;
}
Не бойтесь, я обычно не даю определение в таких классах. Я просто сделал это здесь, чтобы надеюсь улучшить читабельность. Во всяком случае, вывод:
size of 100 instances of Real including padding is 1600 bytes
size of 100 instances of Fake including padding is 800 bytes
real::method2() = 123
fake::method2() = 456
derived::method2() = 789
Real::method0()
Fake::vmethod0(0x1bd8040)
DerivedFake::vmethod0(0x1bd8060)
Real::~Real()
Fake::vdestructor(0x1bd8040)
DerivedFake::vdestructor(0x1bd8060)
Fake::vdestructor(0x1bd8060)
Возможно, он не является потокобезопасным, может содержать устрашающий легион ошибок, а также может быть относительно неэффективным, но я надеюсь, что он демонстрирует мою концепцию. Он был протестирован на 64-битной Ubuntu с g++-4.7. Я сомневаюсь, что в 32-битных системах есть какое-либо преимущество в размере, и, поскольку я сохраняю меньше слова (4 байта, так много для этого!), Мне пришлось поместить туда поле, чтобы показать эффекты. Не стесняйтесь тестировать скорость (пожалуйста, сначала оптимизируйте ее, если вы это сделаете, я поторопился с этим) или протестируйте эффекты на других архитектурах / платформах и с другими компиляторами (я хотел бы увидеть результаты, поэтому, пожалуйста, поделитесь ими, если вы это сделаете) ). Нечто подобное может быть полезно, когда кто-то находит необходимость создать 128/256-битную платформу, создает процессор с очень ограниченной поддержкой памяти, но невероятной скоростью или с компиляторами, которые используют около 21 байта для виртуальной таблицы на каждом экземпляре.
изменить:
Упс, пример кода был сумасшедший. Починил это.
an index for a table containing pointers to tables containing pointers to functions
. Мне действительно интересно понять, сколько памяти в простом сканарио (100 классов с 10 виртуальными функциями в каждом) точно будет использовать ваша реализация. - person   schedule 26.07.2012