Hacker News new | past | comments | ask | show | jobs | submit login

Just go all out C-style, forgoing any virtual functions and instead only use struct-of-pointers instead of this weird construction.



You... haven't worked on a large codebase, have you?

The part that baffles me about this entire post is that it's trivial to obtain pointers to member functions legally, without the fragility associated with guessing VTable offsets.


If you're referring to C++ pointer-to-member types, the semantics of those still requires them to perform virtual dispatch at point of use. That is, given:

   struct Base { virtual void foo(); }
   struct Derived: Base ( virtual void foo(); }
   auto p = &Base::foo;
   Derived d;
   (d.*p)();
The last line must invoke Derived::foo, not Base::foo. Which in turn means that the representation of a pointer-to-member-function cannot be a simple function pointer in the most general case. The representation must store enough information to know whether it's pointing to virtual or non-virtual member, and then store either direct pointer to code or vtable offset depending on that.

Consequently, an indirect call via pointer-to-member has to do the virtual/non-virtual check first, and then look up vtable by index if virtual, before performing the actual call. Which is in fact more work than doing the virtual call directly (since the first step is unnecessary in that case), and thus pointers-to-members cannot be used to optimize away the overhead.


> the representation of a pointer-to-member-function cannot be a simple function pointer

Sure it can, you just need to have emitted separate functions for "concrete call" (the traditional one stored in the vtable) and "virtual call" (a bit of code that calls through the vtable).

If this is not done it's probably due to the fact that actually emitting the virtual calls involves more code than just storing the vtable index. But if anyone is implementing a new language I strongly suggest paying the cost anyway, for sanity.


There are other complications at play for your proposed solution.

If you emit a stub for each virtual member function that performs the vtable dispatch, at which point do you emit it? If you do it if and when the address is actually taken, then pointers to members in different compilation units will have different stubs (COMDAT folding can take care of this for static linking, but not for dynamic) - which means that &Foo::bar taken in one unit will not compare equal to the same taken in another, which is counter to the C++ specification.

Alternatively, you could emit such a stub proactively for each vtable entry on the basis that its address might be taken in another compilation units - but that means a lot of unused stubs.

The other problem is multiple inheritance combined with upcasting. Consider:

   struct Base1 { ... }
   struct Base2 { virtual void foo(); }
   struct Derived: Base1, Base2 { ... }
   void (Derived::*pf)() = &Base2::foo;
   Derived d;
   (d.*pf)();
The problem here is that your stub for Base2::foo expects `this` to point at the beginning of Base2. But then when it is invoked on an instance of Derived, what's readily available at call site is a pointer to the beginning of Derived. Which is not the same, because Base2 is the second subobject of the Derived instance - so you need to adjust that pointer to Derived by sizeof(Base1) chars to upcast it to a pointer to Base2. However, you don't know at call site that your Derived::* actually points to a member of Base2 in the first place - so the information about this adjustment has to be stored in the pointer, as well, and has to be dynamically loaded and applied at call site (which is yet another step making it slower than a regular virtual call, by the way). And this cannot be done with pre-generated stubs since you don't know in advance which classes in other compilation units might inherit from Base2 later on, and what layout they are going to have.

If anyone is designing a new language, I strongly suggest not doing anything like C++ unbound member function pointers in the first place; the practical need for such a thing is very rare, since in most cases you want the pointer to be bound to a particular object (and then vtable resolution can be done at the point where the pointer is produced, rather than at the point where it's used). And, on the other hand, in those rare cases where such a thing is needed, it can be trivially done with closures if the language has those (and it should, given their much broader utility). If C++ had lambdas from the get go, I very much doubt that it would also have pointers to member functions.


Ah right, that.

To be clear to others: the problem appears due to the fact that `R (Base::*)(A...)` can be cast to `R (Derived::*)(A...)` (note the contravariance), not just at construction time but arbitrarily later. This is unfortunately special to the `this` argument; a sane language should allow contravariance of any function pointer argument. (this implies that, if you're emitting C code, all arguments should unfortunately be `Object ` to avoid illegal casts.

I actually disagree that closures fix this though - you can easily end up with deeply nested closures which are opaque so you can't optimize them out. Rather, just make unbound functions identical to free functions.

And yes, I agree that single-inheritance + traits (~ late interfaces, ~ external vtables, which turn out to be easier to think about inlining. You can't use instanceof though) is much saner than multiple-inheritance + ADL. Honestly, I'd say that ADL is C++'s worst mistake, since it intrudes upon every* function call you make, unlike most of the crazy stuff we're doing here.

Doing without single-inheritance is in fact viable but annoying, especially when porting code or programmers. Emulating single-inheritance in terms of traits can be done by splitting each class into a concrete struct + a trait (and aggregating the structs but inheriting the traits), but this requires manually forwarding all the unchanged virtual functions, and also trait pointers are fat which is often undesirable (except note that for small types you often don't want to pay for an embedded vtable pointer). Moving that back to a class inheritance system, however ... I've often thought it is quite useful to support both "pointer to class, virtually" and "pointer to class, final".


> I actually disagree that closures fix this though - you can easily end up with deeply nested closures which are opaque so you can't optimize them out. Rather, just make unbound functions identical to free functions.

It would be trivial to optimize - as that would be a lambda of the form `[](Foo& foo, ...) { foo.bar(...); }` it can just be emitted as a free function with no closure, no matter how deeply nested it is. In fact, looking at it in Godbolt, it seems that compilers do this even when optimizations aren't enabled at all, probably because it's just the most obvious way to codegen.

Effectively, it just becomes a way to define a regular function in-place at the point where its address gets taken. In C++, such a lambda is even implicitly convertible to the corresponding plain function pointer type, so it's already unified with regular free functions on type system level.

Of course, you could then say that &Foo::bar is simply syntactic sugar for such a lambda. I think we agree that the important part here is to avoid having the special notion of pointers-to-member-functions in the type system; the rest is just a question of how to dress it up when you have a use case where you need one.


Exactly. Don't do this. UB is fragile.

When downshifting to C, use opaque structs to hide private data and use regular function pointers. There are numerous examples of C VTables but these tend to be slow. When possible, don't use OOP or VTables because it's slow and more difficult to debug. Using plain functional and procedural architectures is much more robust, deterministic, testable, and easier to reason about.

https://godbolt.org/z/feajn1W4j


I meant instead of casting the vtable pointer and guessing the layout (imagine doing that in a large codebase...), just define the struct yourself and disallow the virtual functions in this case specifically if you're chasing the performance that badly.


There is one quite well known and very large codebase that does this extensively: the Linux kernel.

I have mixed opinions about it.


80% of the code is drivers, which having written many myself, doesn't always need the latest OOP features. Abstractions suck in C for sure, but it's OK.

Meanwhile, the Windows XP codebase had 45M LOC in 2011.

Truly big codebases need to be written in sane languages, mostly OOP ones. That excludes C, Zig and Rust.


Virtual functions in C++ are effectively implemented as struct-of-pointers. Demonstrating how that works is a large proportion of the article.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: