All your base do not belong to you

All your base do not belong to you

Rate This
  • Comments 9

People sometimes ask me why you can’t do this in C#:

class GrandBase
{
  public virtual void M() { Console.WriteLine("GB"); }
}

class Base : GrandBase
{
  public override void M() { Console.WriteLine("B"); }
}

class Derived : Base
{
  public override void M()
  {
    Console.WriteLine("D");
    base.base.M(); // illegal!
  }
}

The author of the most-derived class here wishes to call its GrandBase implementation of M, rather than the Base implementation of M. It wishes to do an "end-run" around its base class.

This is not legal in C#, and is a bad programming practice. If you find yourself in a position where you want to do an end-run around your base class, odds are good that there is a larger flaw in your type hierarchy that needs to be fixed.

The fact that Base is derived from GrandBase is a “public” part of the surface area of Base. But the fact that Base.M overrides GrandBase.M is not part of that public surface area; by inheriting from GrandBase, Base is promising to provide an implementation of M, but whether that implementation is an override, or merely defers directly to the GrandBase implementation is an implementation detail of Base. The Derived class should not know or care how Base fulfills its contract; merely that it does so satisfactorily.

Moreover, when the author of Derived derived it from Base, presumably that developer did so because they liked Base and wanted to re-use its implementation details. If you don’t like the implementation details of Base then don’t derive from it in the first place; derive from GrandBase directly.

This is also a bad idea and therefore illegal because it is fragile. Suppose we have a slightly more complex scenario. Let’s add a protected virtual method P to GrandBase. Suppose GrandBase.M calls P, and suppose Base overrides P. If Base.M sets up some state that Base.P depends on, then Derived doing an end-run around Base.M means that GrandBase.M will call Base.P without Base.M setting up the state it needs.

class GrandBase
{
  public virtual void M() { Console.WriteLine("GB"); P(); }
  protected virtual void P() { }
}

class Base : GrandBase
{
  public override void M()
  {
    Console.WriteLine("B");
    // We know there is about to be a call to P.
    SetUpStateForP();
  }

  protected override void P()
  {
     // Use the state set up by M
     ...

This could have an impact on security or correctness. It is hard enough to design correct, secure, robust implementations of virtual methods; let’s not make it any harder.

Now, I note that this is only a rule of C#, not a rule of the CLR. The CLR does allow a language to implement a feature whereby a non-virtual call is done to a virtual method that skips arbitrarily far down the class hierarchy. You cannot rely upon the CLR enforcing this rule of C#. However, the CLR does not allow non-virtual calls from a non-derived type. If you made another type, C, that was not derived from GrandBase then a non-virtual call to GrandBase.M would not be verifiable. Interestingly enough, this rule applies even to nested types; the CLR verifier does not allow a nested type to do a non-virtual call to a virtual method of its container's base class.

  • If you had to do it again, would you make nested types' non-virtual calls to virtual methods of their container's base verifiable? The upside is that it would make the use case of calling "base" methods from closures verifiable. What would be the downside?

  • Great article. I know I tried to do something like that before, and it felt wrong, but I couldn't put my finger on exactly what was wrong with it...

    I love the Zero Wing reference in the title, but I think there's a mistake, it should be "All your base ARE not belong to you" ;)

  • I've seen this kind of issue arise quite often in UI frameworks that rely on inheritance to compose functionality.

    So, for instance, you may have some base class Control (for instance) that provides virtual methods for doing things like layout rendering visual chrome. You then have directed classes that extend Control and provide custom behavior by overriding these methods. So let's say there's a TextBox class inheriting from Control that overrides Layout( ) and performs it's own custom logic, and then delegates back to Control to allow some common layout logic to be applied as well. Developers who inherit from TextBox and who desire most of the functionality that TextBox adds but want to override layout processing are in a bit of a bind. They can override Layout( ) but they can't call down into Control to invoke the common layout processing without calling through TextBox's implementation of Layout( ) ... which is exactly what they don't want.

    It's cases like this that are hard to support when inheritance is used to propagate common behavior to a set of related classes.

  • "odds are good that there is a larger flaw in your type hierarchy that needs to be fixed."

    "your" is important. Situations like this arise when GrandBase and Base are not "your" classes. And you want your class (Derived) to do something very similar to Base, but a little bit different. Reimplementing all of Base's logic is often not a good solution either.

  • "But the fact that Base.M overrides GrandBase.M is not part of that public surface area"

    Unfortunately this is only true if you're just interested in source compatibility, not binary compatibility. Can't find your blog post on it. Interestingly from what I remember you argued that removing/adding an override is an obvious breaking change.

    And I find it a bit annoying that calling `base.base.M()` from another language is possible/creates verifiable code. It might cause people not aware of this to create insecure code where they believe that grandbase.M() is secured from the grandchildren because it was overriding in base.M(). Or as a similar discrepancy the `this!=null` rule from C#.

    But I guess that's the price we have to pay for having a multi language runtime, since languages like C++ allow calling such methods.

  • Great post. I know a few people who asked that same question, now I can just point them to your post. Just one little detail you might want to fix: in the last example, in Base.M, you might want to add a call to base.M() after SetUpStateForP(), so that P() would get executed in your example code. It's not necessary to get the point across, but it stops people from wondering "wait, why are we setting up state for P, if P is not really getting called".

  • Excellent post, thanks

  • @CodeInChaos I think the post you are referring to is blogs.msdn.com/.../putting-a-base-in-the-middle.aspx

    The gist is that if Derived lives in separate assemby, and you add an override in Base without recompiling Derived (let's call it Foo()), then any calls to Foo() from Derived will go to GrandBase, and not Base until you recompile Derived.

  • @AC yes that's exactly the blog entry I was talking about. And the annoying thing about it is that it makes the fact that M has been overridden in `Base` part of the public binary api even so it should be an implementation detail of `Base`.

    So if one want binary compatibility with future versions which might override a virtual method one should override all virtual functions in `Base` even if you only call `GrandBase` in the implementation.

Page 1 of 1 (9 items)