Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the advantage of the "hand-rolled" vtable approach?

Recently, I've come across a couple of type-erasure implementations that use a "hand-rolled" vtable - Adobe ASL's any_regular_t is one example, although I've seen it used in Boost ASIO, too (for the completion routine queue).

Basically, the parent type is passed a pointer to a static type full of function pointers defined in the child type, similar to the below...

struct parent_t;

struct vtbl {
  void (*invoke)(parent_t *, std::ostream &);
};

struct parent_t {
  vtbl *vt;
  parent_t(vtbl *v) : vt(v) { }
  void invoke(std::ostream &os) {
    vt->invoke(this, os);
  }
};

template<typename T>
struct child_t : parent_t {
  child_t(T val) : parent_t(&vt_), value_(val) { }
  void invoke(std::ostream &os) {
    // Actual implementation here
    ...
  }
private:
  static void invoke_impl(parent_t *p, std::ostream &os) {
    static_cast<child_t *>(p)->invoke(os);
  }
  T value_;
  static vtbl vt_;
};

template<typename T>
vtbl child_t<T>::vt_ = { &child_t::invoke_impl };

My question is, what is the advantage of this idiom? From what I can tell, it's just a re-implementation of what a compiler would provide for free. Won't there still be the overhead of an extra indirection when parent_t::invoke calls vtbl::invoke.

I'm guessing that it's probably got something to do with the compiler being able to inline, or optimize out the call to vtbl::invoke or something, but I'm not comfortable enough with Assembler to be able to work this out myself.

like image 682
gmbeard Avatar asked Nov 02 '15 19:11

gmbeard


1 Answers

A class having a useful vtable basically requires that it be dynamically allocated. While you can do a fixed storage buffer and allocate there, it is a hassle; you don't have reasonable control over the size of instances once you go virtual. With a manual vtable, you do.

Glancing at the source in question, there are a lot of asserts about the size of various structures (because they need to fit in an array of two doubles in one case).

Also a "class" with a hand-rolled vtable can be standard layout; certain kinds of casting becomes legal if you do this. I don't see this being used in the Adobe code.

In some cases, it can be allocated separately from the vtable entirely (as I do when I do view-based type erasure: I create a custom vtable for the incoming type, and store a void* for it, then dispatch my interface to said custom vtable). I don't see this being used in the Adobe code; but an any_regular_view that acts as a pseudo-reference to an any_regular might use this technique. I use it for things like can_construct<T>, or sink<T>, or function_view<Sig> or even move_only_function<Sig> (ownership is handled by a unique_ptr, operations via a local vtable with 1 entry).

You can create dynamic classes if you have a hand-rolled vtable, where you allocate a vtable entry and set its pointers to whatever you choose (possibly programmatically). If you have 10 methods, each of which can be in one of 10 states, that would require 10^10 different classes with normal vtables. With a hand-rolled vtable, you just need to manage each classes' lifetime in a table somewhere (so instances don't outlive the class).

As an example, I could take a method, and add a "run before" or "run after" method to it, on a particular instance of a class (with careful lifetime management), or on every instance of that class.

It is also possible that the resulting vtables might be simpler than compiler-generated ones in various ways, as they aren't as powerful. Compiler-generated vtables handle virtual inheritance and dynamic casting, for example. The virtual inheritance case might have no overhead unless used, but dynamic casting may require overhead.

You can also gain control over initialization. With a compiler-generated vtable, the status of the table is defined (or left undefined) as the standard dictates: with a hand-rolled one, you can ensure any invariants you choose hold.

The OO pattern existed in C before C++ came around. C++ simply chose a few reasonable options; when you go back to pseudo-C style manual OO, you get access to those alternative options. You can dress things up (with glue) so that they look like normal C++ types to the casual user, while inside they are anything but.

like image 171
Yakk - Adam Nevraumont Avatar answered Oct 02 '22 03:10

Yakk - Adam Nevraumont