2

I am working on a program language, and I came to the dilemma whether it should support virtual inheritance or not.

As a designer and implementer of the language, including that feature represents added complexity.

As a language agnostic software developer, it seems like the only benefit from including it is to facilitate and allow bad software design.

The cases I encountered where virtual inheritance is utilized all seem to be better candidates for "add-on" interfaces.

Of course, I do recognize that working with existing 3rd party code you don't have control over is definitely something that might mandate the support for virtual inheritance.

But for me this is not the case, I have a clean slate, and full freedom to define the paradigms and idioms of the programming language. Therefore the more elegant and efficient solution seems to be to simply eliminate the possibility that the need for virtual inheritance might arise and omit the feature altogether.

I also notice that it is a rather niche feature, of the several languages I have studied, C++ is the only one to support it. Leading me to presume it is not all that essential.

Naturally, I might also be overlooking something, which prompted me to probe the developer community for input on the subject.

EDIT: To clarify as requested, performance and memory efficiency are some of the primary goals of the language. Virtualism, dynamism and any high level programming functionality is optional and only included if necessary. It is not a language where everything is references and virtual calls.

Instead of

class WingedAnimal : extend Animal {}

my current plan is to avoid the need to address member duplication by means of

class WingedAnimal : require Animal {}

which essentially allows WingedAnimal to use Animal without inheriting it, guaranteeing that any users of WingedAnimal will be compatible.

dtech
  • 763

2 Answers2

3

Virtual inheritance as used by some C++ implementations only makes sense under fairly specific constraints:

  • Classes have a fixed object layout that is known at compile-time.
  • An instance can be upcasted to a base class type. Therefore, inheritance must embed the base class layout into the subclass layout.
  • The language allows multiple inheritance.
  • In order to avoid duplicate embeddings with multiple inheritance, the position of the base class layout is not fixed but is available through additional pointer indirection.

Because of this indirection, virtual inheritance necessarily implies an unavoidable time overhead at runtime. Either your language will have to pay this overhead for all classes, or have to make a distinction between normal classes and MI-safe classes. MI with normal classes is safe as long as they don't lead to multiple embeddings, but this e.g. means that inheriting from another base class is not longer a backwards-compatible change.

The alternative approach is that class layouts are not fixed, but that MI-capable classes must only access instance fields through virtual methods/properties. The layout and these properties are then provided by the most-derived class that is actually instantiated. This is e.g. the approach that C# uses, where interfaces cannot declare fields but can declare (virtual) properties. This has a smaller performance impact than you might think due to to JIT compilation with extremely clever inlining. Similar approaches are used by some trait implementations, since traits cannot declare fields but can require and provide methods.

Most MI systems simply do not assume a fixed object layout but resolve methods and fields by name. E.g. this is fundamental to Python's MI approach. However, it seems that this might be undesirable for your language.

Any of these solutions imply some overhead. Virtual inheritance possibly implies the least overhead, but it has a strong impact on language ergonomics: classes have to explicitly choose between performance and MI-capability. The easiest way to solve this dilemma is to make a fundamental language design choice. Either decide to forbid implementation-MI, or decide to relinquish C/C++'s performance and “don't pay for what you don't use” ethos. I'd strongly consider the latter option because it has worked well in practice: most programs simply don't need every last bit of available performance.

MI and OOP implementations in general pose more difficult problems than just object layout (to which virtual inheritance is a possible solution). Additionally, you have to think about method dispatch strategies (e.g. compare C++-like multiple vtables per object, Java/C#-style single vtable + interface table search, Go/Rust-style fat pointers where the object doesn't have a vtable member, and hash table based dispatch as in Python). Consider also the problem of initialization order in a MI class. E.g. in Python all constructors in a MI hierarchy need to have the same signature which is extremely undesirable.

To be absolutely clear, this answer assumes that you do in fact want to support dynamic dispatch. Inheritance then doesn't just include the base class implementation like a mixin, but also allows dynamic dispatch through the base class interface. The MI difficulties are largely about maintaining a base-class compatible interface. If you get rid of that so that all method calls and field accesses go through the statically known subclass, then you don't need typical inheritance machinery like vtables or virtual inheritance – you are free to compute an arbitrary layout for each class.

amon
  • 135,795
0

In many ways, implementation inheritance of any kind is out-of-favour these days. The common ethos today is to prefer aggregation over inheritance. This suggests that a solution to problems of how to implement inheritance is to avoid the issue entirely: only allow interface inheritance (ie subtyping). Instead, provide useful tools to make aggregation easier: declarative delegation of unimplemented methods of an interface to a particular member, for example, could be a different way of achieving the same results that may be clearer and easier to understand.

Jules
  • 17,880
  • 2
  • 38
  • 65