首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >实施vptr的替代方案?

实施vptr的替代方案?
EN

Stack Overflow用户
提问于 2012-04-12 22:02:04
回答 4查看 877关注 0票数 7

这个问题不是关于C++语言本身(即不是关于标准的),而是关于如何调用编译器来实现虚拟函数的替代方案。

实现虚函数的一般方案是使用指向指针表的指针。

代码语言:javascript
复制
class Base {
     private:
        int m;
     public:
        virtual metha();
};

同样地,在C语言中,类似于

代码语言:javascript
复制
struct Base {
    void (**vtable)();
    int m;
}

第一个成员通常是指向虚拟函数列表等的指针(内存中应用程序无法控制的一块区域)。在大多数情况下,在考虑成员之前,这会花费指针的大小,等等。所以在32位寻址方案中,大约4个字节,等等。如果你在你的应用程序中创建了一个40k多态对象的列表,在任何成员变量之前,这大约是40k x 4字节= 160k字节,等等。我也知道这恰好是C++编译中最快和常见的实现。

我知道这会因为多重继承而变得复杂(特别是在它们里面有虚类,比如菱形结构等)。

另一种方法是将第一个变量作为vptrs表的索引id (在C中等效,如下所示)

代码语言:javascript
复制
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%。这主要与大量创建的小多态有关,所以我想知道,如果不是整个应用程序,这种方案是否可能至少适用于特定的虚拟对象。

EN

回答 4

Stack Overflow用户

发布于 2012-04-12 22:12:53

您的建议很有趣,但如果可执行文件由多个模块组成,并在这些模块之间传递对象,那么它就不会起作用。假设它们是单独编译的(比如classid ),如果一个模块创建了一个对象并将其传递给另一个模块,而另一个模块调用了一个虚方法-它如何知道DLL引用的是哪个表?您将无法添加另一个moduleid,因为这两个模块在编译时可能不知道对方。所以除非你使用指针,否则我认为这是个死胡同...

票数 3
EN

Stack Overflow用户

发布于 2012-04-12 22:20:51

以下是一些观察结果:

  1. 是,可以使用较小的值来表示类,但一些处理器要求数据对齐,因此可能会因为要求将数据值对齐到4字节边界而损失空间节省。此外,对于多态继承树的所有成员,class-id必须在一个定义好的位置,因此它很可能在其他日期之前,因此无法避免对齐问题。
  2. 存储指针的成本已经转移到代码中,在代码中,每次使用多态函数都需要代码将class-id转换为vtable指针或某种等效的数据结构。所以它不是免费的。显然,成本权衡取决于代码量与对象数量。

_

  1. 如果对象是从堆中分配的,则通常存在浪费的空间,以确保对象被分配到最差的边界,因此,即使存在少量的代码和大量的多态对象,存储器管理开销也可能显著大于指针和字符之间的差值。

_

  1. 为了允许独立编译程序,整个程序中的类的数量,因此在编译时必须知道类id的大小,否则,无法编译代码来访问它。这将是一项重要的开销。更简单的做法是在最坏的情况下修复它,并简化编译和链接。

请不要停止我的尝试,但是使用任何使用可变大小id来派生函数地址的技术都有更多的问题需要解决。

我强烈建议您也在Wikipedia Cola上查看Ian Piumarta's Cola

它实际上采用了一种不同的方法,并以更灵活的方式使用指针来构建继承,或基于原型,或开发人员所需的任何其他机制。

票数 3
EN

Stack Overflow用户

发布于 2012-04-12 22:54:39

不,没有这样的开关。

LLVM/Clang代码库避免了由数万人分配的类中的虚拟表:这在closed层次结构中工作得很好,因为单个enum可以枚举所有可能的类,然后每个类都链接到enum的一个值。closed显然是因为enum

然后,通过enum上的switch实现虚拟化,并在调用该方法之前进行适当的强制转换。再一次关闭了。必须为每个新类修改switch

第一种选择是:外部vpointer。

如果您发现自己处于一种经常支付vpointer税的情况下,那就是大多数对象都是已知类型的。然后,您可以将其外部化。

代码语言:javascript
复制
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); }

然后,想象一个简单的类:

代码语言:javascript
复制
class Counter {
public:
  int getCount() const { return c; }
  void updateCount(int i) { c = i; }
private:
  int c;
};

您可以将对象存储在数组中:

代码语言:javascript
复制
static Counter array[5];

assert(sizeof(array) == sizeof(int)*5); // no v-pointer

并且仍然将它们用于多态函数:

代码语言:javascript
复制
void five(Interface& i) { i.updateCount(5); }

InterfaceBridge<Counter> ib(array[3]); // create *one* v-pointer
five(ib);

assert(array[3].getCount() == 5);

vs引用的值实际上是一种设计张力。一般来说,如果您需要clone,您需要按值存储,并且当您按基类(例如boost::ptr_vector)存储时,您需要克隆。实际上可以同时提供两个接口(和网桥):

代码语言:javascript
复制
Interface <--- ClonableInterface
  |                 |
InterfaceB     ClonableInterfaceB

只是额外的打字。

另一种解决方案,涉及更多。

开关可以通过跳转表来实现。这样的表可以在运行时完美地创建,例如在std::vector中:

代码语言:javascript
复制
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类:

代码语言:javascript
复制
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
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/10125140

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档