multiple inheritance and (not) overriding functions

3 views
Skip to first unread message

Stephan Weinberger

unread,
Jul 18, 2026, 4:52:02 PM (4 days ago) Jul 18
to LDMud Talk
Hello,

I just stumbled upon some unexpected (to me, at least) behaviour (ldmud
3.6.8, but probably that way since the dawn of time):

---- foo.c ----

void func() { write("foo"); }
void foo() { func(); }


---- bar.c ----

void func() { write("bar"); }
void bar() { func(); }


---- baz.c ----

private inherit "foo";
private inherit "bar";

void baz() { bar::bar(); }


Here calling baz() produces output "foo"!

If I called func() in baz.c itself it would be totally expected that
there is some precedence (as documented in concepts/inheritance, "3. The
first inherited function has precedence.").

Also, of course, I'd expect that to happen if func() is overridden in
baz.c, as that's one of the main points of inheritance.

But is it intentional, that the inheritance is resolved "around the
corner" and bar.c sees a function that is defined in a different
*branch* of the inheritance tree? The documentation only says "the
compiler chooses a dominant definition that will finally be visible" but
doesn't allude to where this 'visibility' extends to. From the observed
behaviour it's the whole program including *every* inherit up & down the
whole tree, whereas the function/inherit visibility modifiers only work
one-way (up the tree).

Hence it's not necessarily intuitive that this would also affect
*different* branches in a multiple-inheritance scenario (i.e. that
foo::func() behaves as if it were *defined* *in* baz.c, not just
inherited from elsewhere). Particularly when that makes it impossible to
call bar::func() from within bar itself, since there is no "self" or
"just my inheritance branch" operator available.


On the other hand, defining the functions as 'private' actually *does*
stop them from being overridden (which is also documented in
concepts/inheritance), so there is already support to resolve
inheritance in different ways and the compiler behaves differently
depending on the function modifiers.

Alas, using 'private' is not really a solution if the inheriting program
*should* be able to call the function (i.e. it can only be 'protected'
at most). But this does mean that the functionality is already there --
it's just not accessible in a useful way!


Suggestion 1

Hence I suggest to extend the current behaviour for 'private' functions
to also include 'protected' ones (which coincidentally is already a nice
name for "protecting them from being overriden"). Since this only uses
existing functionality I guess it would be quite easy to change.

That way there would be more fine-grained control over how/if functions
that are visible to the inheriting program can be overridden (private:
not visible & not overridable; protected: visible, but not overridable;
visible/public: visible & overridable). Visibility to the outside via
call_other() would still be covered by the inherit modifier (e.g. a
'visible' function that is intended to be overridden can still be made
'protected/private' to the outside by using 'protected/private inherit').

If backwards compatibility is an issue (although I don't think anyone
actively/knowingly uses this, and I actually suspect that this has
already caused some headaches in the past which just "magically
disappeared" by re-ordering the inherits), the inclusion/exclusion of
'protected' could be toggled with a compile-time option (were I would
make including it the new default) or a #pragma.


Alternatively we could introduce a more broad #pragma or modifier
'no_override' (i.e. replicate what inheriting 'private' functions
already does, but more explicit and as distinct option for any function
regardless of visibility), or even a new keyword like 'use "bar.c"' to
"inherit without overriding functions" (i.e. do the opposite, and
prevent functions from this inherit from overriding others -- which is
currently only partially covered by the order of inheritance, as the
first inherit will always override the others).

Since this is behaviour I mostly want to control from the side of the
program that is *being* inherited, the #pragma or function modifier
seems more practical, but a modifier to the 'inherit' statement might
also have its uses in more complex inheritance trees.


Suggestion 2

My second big wish would still be a 'self::func()', to make sure I can
always call my own version of a function. Or, since the syntax allows
for filenames, maybe something like '"."::func()', similar to the
already existing "*".


Suggestion 3

This would probably be a bigger change, but generally I suggest to *not*
resolve inheritance/overrides "back down" into different branches, but
only "upwards" the inheritance graph by default.

Maybe make the current behaviour available via a #pragma or something
like "+"::func() if someone really needs it, even though I cannot think
of a scenario where I would want that.
When writing an inheritable program I can't possibly know what other
programs are eventually going to be inherited *besides* it. And those
are also very likely to implement completely different things (hence why
they are in a separate inheritance branch to begin with). I.e. mixing
them and having some functions accidentally being overridden in a
non-obvious way is almost guaranteed to cause problems.

I can live with the ambiguity (or the defined precedence due to the
order of inheritance) in one direction, as long as I can at least
control the other direction. If I want to override functions from
specific and/or multiple inherits I can always do that with a "shim" in
*those* particular inherit branches (albeit one could think about
syntactic sugar like 'foo::' also working in function definitions, to
override that specific inherit). But conversely, with the current
behaviour, I have no way of preventing the environment a program is
eventually used in (which is usually out of my control) from messing up
the functionality.

In my case I only found out by chance because one of the programs is a
"utility class" for security checks that raises errors when called in
the wrong way. If the code had just quietly done its (in this case very
wrong) thing I'd never have noticed.


regards,
  Invis


P.S. and no, just calling the functions differently isn't a solution
IMHO. Some trivial function names are just always going to overlap. This
is just an unavoidable consequence of the traditional LPC way of
inheriting multiple things to build a functioning program/object. Going
forward, lwobjects will probably alleviate that to some extent, but they
can also only do so much.
In this context inherits and the '::' syntax already offer a nice way of
creating "namespaces", so why not use that feature...

Also, including distinct identifiers, like e.g. the filename, in all
function names (i.e. 'foo_func()' and 'bar_func()') -- while
functionally similar to the '::' syntax -- would make the intentional
use of inheritance and overriding more cumbersome (apart from just being
unnecessary clutter). Particularly if I want to override functions from
deeper down the tree, which pulls those unique names up the chain again
into programs of a different name.

Stephan Weinberger

unread,
Jul 19, 2026, 4:47:21 PM (3 days ago) Jul 19
to ldmud...@googlegroups.com
Addendum:

Another alternative would be to allow 'nomask' functions of the same
name in different inherit branches (i.e. *only* prohibit overriding them
at a *higher* level of the inherit tree), and treat calls to them with
the same logic as private/non-private.

The nomask keyword also would be a fitting name for "don't override",
and actually does exactly that, just *too* strictly by only allowing a
single nomask function of a certain name in the whole inheritance tree.
Even the precedence rules for inheritance ("2. nomask takes precedence
over non-nomask") could stay the same, as multiple nomask definitions
would still be covered by rule no. 3 "first inherit takes precedence".


regards,
  Invis


PS. I opened a PR to extend the documentation in
/doc/concepts/overloading to include this behaviour.

PPS. I'm using the term "override" for what the ldmud documentation
calls "overload" -- this is a German vs. English thing: The English
"overload" is more what German software developers would typically refer
to as  (compile-time polymorphism), i.e. the same function name with
different signatures (which LPC somewhat already has with varargs &
union types, so it isn't really a thing here), vs. the English use of
"override" for what Germans would call 'überladen' (i.e. literally
'over-load') for run-time polymorphism. 
Reply all
Reply to author
Forward
0 new messages