这个问题不是关于C ++语言本身(即不是关于标准),而是关于如何调用编译器来实现虚函数的替代方案。
实现虚函数的一般方案是使用指向指针表的指针。
class Base {
private:
int m;
public:
virtual metha();
};
等效地说C会像
struct Base {
void (**vtable)();
int m;
}
第一个成员通常是指向虚拟函数列表等的指针(应用程序无法控制的内存中的一块区域)。在大多数情况下,这会在考虑成员之前花费指针的大小等等。因此在大约4个字节的32位寻址方案中等等。如果在应用程序中创建了40k多态对象的列表,则大约为40k x在任何成员变量等之前4个字节= 160k字节。我也知道这恰好是C ++编译中最快和最常见的实现。
我知道多重继承很复杂(尤其是虚拟类,即菱形结构等)。
另一种方法是将第一个变量作为vptrs表的索引id(等效于C,如下所示)
struct Base {
char classid; // the classid here is an index into an array of vtables
int m;
}
如果应用程序中的类总数小于255(包括所有可能的模板实例化等),则char足以保存索引,从而减少应用程序中所有多态类的大小(我是排除对齐问题等。)
我的问题是,在GNU C ++,LLVM或任何其他编译器中是否有任何切换来执行此操作?或减少多态对象的大小?
编辑:我了解指出的对齐问题。还有一点,如果这是64位系统(假设为64位vptr),每个多态对象成员的成本约为8字节,那么vptr的成本就是内存的50%。这主要涉及大量创建的小型多态,所以我想知道如果不是整个应用程序,这个方案是否至少可以用于特定的虚拟对象。
答案 0 :(得分:3)
您的建议很有意思,但如果可执行文件由多个模块组成,并在其中传递对象,则无效。鉴于它们是单独编译的(比如DLL),如果一个模块创建一个对象并将其传递给另一个,另一个模块调用一个虚方法 - 它如何知道classid
引用哪个表?您将无法添加另一个moduleid
,因为这两个模块在编译时可能不会彼此了解。所以除非你使用指针,否则我认为这是一个死胡同......
答案 1 :(得分:3)
有几点意见:
是的,可以使用较小的值来表示类,但是某些处理器需要对齐数据,以便通过将数据值与例如对齐的要求可能会丢失空间节省。 4字节边界。此外,class-id必须位于多态继承树的所有成员的明确定义的位置,因此它可能超过其他日期,因此无法避免对齐问题。
存储指针的成本已经移到代码中,每次使用多态函数都需要代码将class-id转换为vtable指针或某个等效的数据结构。所以它不是免费的。显然,成本权衡取决于代码量与对象的数量。
如果从堆中分配对象,则通常会在orer中浪费空间以确保对象被调到最差边界,因此即使存在少量代码和大量多态对象,内存管理开销明显大于指针和char之间的差异。
为了允许程序独立编译,整个程序中的类数量,以及类ID的大小必须在编译时知道,否则代码无法编译访问它。这将是一个重大的开销。在最坏的情况下修复它更简单,并简化编译和链接。
请不要让我阻止你尝试,但是使用任何可能使用可变大小id来获取函数地址的技术来解决还有很多问题。
我强烈建议您Ian Piumarta's Cola
查看Wikipedia Cola它实际上采用了不同的方法,并以更灵活的方式使用指针,以构建继承,或基于原型,或开发人员需要的任何其他机制。
答案 2 :(得分:3)
不,没有这样的转换。
LLVM / Clang代码库避免了由成千上万的类分配的类中的虚拟表:这在封闭层次结构中运行良好,因为单个enum
可以枚举所有可能的类然后每个类都链接到enum
的值。 已关闭显然是因为enum
。
然后,虚拟性由switch
上的enum
实现,并在调用方法之前进行适当的转换。再一次,关闭。必须为每个新类修改switch
。
第一种选择:外部vpointer。
如果您发现自己处于过度支付vpointer税的情况,那么大部分对象都是已知类型。然后你可以外化它。
class Interface {
public:
virtual ~Interface() {}
virtual Interface* clone() const = 0; // might be worth it
virtual void updateCount(int) = 0;
protected:
Interface(Interface const&) {}
Interface& operator=(Interface const&) { return *this; }
};
template <typename T>
class InterfaceBridge: public Interface {
public:
InterfaceBridge(T& t): t(t) {}
virtual InterfaceBridge* clone() const { return new InterfaceBridge(*this); }
virtual void updateCount(int i) { t.updateCount(i); }
private:
T& t; // value or reference ? Choose...
};
template <typename T>
InterfaceBridge<T> interface(T& t) { return InterfaceBridge<T>(t); }
然后,想象一个简单的课程:
class Counter {
public:
int getCount() const { return c; }
void updateCount(int i) { c = i; }
private:
int c;
};
您可以将对象存储在数组中:
static Counter array[5];
assert(sizeof(array) == sizeof(int)*5); // no v-pointer
仍然使用多态函数:
void five(Interface& i) { i.updateCount(5); }
InterfaceBridge<Counter> ib(array[3]); // create *one* v-pointer
five(ib);
assert(array[3].getCount() == 5);
值与参考值实际上是设计张力。通常,如果需要clone
,则需要按值存储,并且需要在按基类存储时进行克隆(例如boost::ptr_vector
)。实际上可以提供两个接口(和桥接):
Interface <--- ClonableInterface
| |
InterfaceB ClonableInterfaceB
这只是额外打字。
另一个解决方案,更多涉及。
开关可通过跳转表实现。这样的表可以在运行时完美地创建,例如std::vector
:
class Base {
public:
~Base() { VTables()[vpointer].dispose(*this); }
void updateCount(int i) {
VTables()[vpointer].updateCount(*this, i);
}
protected:
struct VTable {
typedef void (*Dispose)(Base&);
typedef void (*UpdateCount)(Base&, int);
Dispose dispose;
UpdateCount updateCount;
};
static void NoDispose(Base&) {}
static unsigned RegisterTable(VTable t) {
std::vector<VTable>& v = VTables();
v.push_back(t);
return v.size() - 1;
}
explicit Base(unsigned id): vpointer(id) {
assert(id < VTables.size());
}
private:
// Implement in .cpp or pay the cost of weak symbols.
static std::vector<VTable> VTables() { static std::vector<VTable> VT; return VT; }
unsigned vpointer;
};
然后,Derived
类:
class Derived: public Base {
public:
Derived(): Base(GetID()) {}
private:
static void UpdateCount(Base& b, int i) {
static_cast<Derived&>(b).count = i;
}
static unsigned GetID() {
static unsigned ID = RegisterTable(VTable({&NoDispose, &UpdateCount}));
return ID;
}
unsigned count;
};
好吧,现在你会意识到编译器为你做了多么棒的事情,即使是以一些开销为代价。
哦,由于对齐,只要Derived
类引入指针,就有可能在Base
和下一个属性之间使用4个字节的填充。您可以通过仔细选择Derived
中的前几个属性来使用它们,以避免填充...
答案 3 :(得分:2)
简短的回答是,不,我不知道有任何转换使用任何常见的C ++编译器。
更长的答案是,为了做到这一点,你只需要将大部分智能构建到链接器中,这样它就可以协调在链接在一起的所有目标文件中分配ID。
我还要指出,它通常不会带来很多好处。至少在典型情况下,您希望struct / class中的每个元素都处于“自然”边界,这意味着它的起始地址是其大小的倍数。使用包含单个int的类的示例,编译器将为vtable索引分配一个字节,紧接着是三个填充的字节,因此下一个int
将落在一个4的倍数的地址。最终的结果是类的对象将占用精确相同的存储量,就像我们使用指针一样。
我补充一点,这也不是一个牵强附会的例外。多年来,标准建议是尽量减少插入结构/类别中的填充,以使预期的项目在开始时最大,并向最小的方向发展。这意味着在大多数代码中,在结构的第一个显式定义的成员之前,你最终会得到相同的三个填充字节。
要从中获得任何好处,你必须要注意它,并且有一个结构(例如)你可以移动到你想要的三个字节的数据。然后你将它们移动到结构中显式定义的第一个项目。不幸的是,这也意味着如果你关闭了这个开关,所以你有一个vtable指针,你最终会得到编译器插入填充,否则可能是不必要的。
总结一下:它没有实现,如果它通常不会实现太多。