digitalmars.com                        
Last update Sat Apr 11 21:16:12 2020

In A Module Far, Far Away Part 2

written by Walter Bright

July 25, 2009

In the last installment, we talked about how changing a declaration in one module can unexpectedly change the behavior of another module, and how language features can mitigate that. Here are some more features of the D programming language designed for that purpose.

Given an enum declaration:

enum E { A, B, C }

and in the remote module there’s a switch statement:

E e;
...
switch (e)
{  case A: ...
   case B: ...
   case C: ...
   default: assert(0);
}

The declaration forE gets updated to add a member D:

enum E { A, B, C, D }

and the switch statement, which is supposed to handle all the possibilities of E, is now incorrect. At least our intrepid programmer has anticipated this and put in a default that asserts. But this identifies the problem at runtime, if the test suites are thorough. We’d prefer to catch it at compile time in a guaranteed manner.

Enter the final switch statement:

final switch (e)
{  case A: ...
   case B: ...
   case C: ...
}

In a final switch statement, all enum members must be represented in the case statements. A default statement has no point, and is not even allowed in a final switch. If we add an unrepresented member D, the compiler dings us in the final switch.

Given a class C declared in one module:

class C { }

and another module declares class D that derives from C, and declares a virtual method foo():

class D : C { void foo(); }

Later, a method foo() is added to class C:

class C { void foo(); }

Now, the call to C.foo() gets inadvertently hijacked by D.foo(), which may be quite unrelated. The solution is to mark intentional overriding with the override keyword:

class B : A { override void bar(); }

Then, if a method overrides a base class method, but is not marked with override, the compiler issues an error.

In module A, there’s a function:

void foo(long x);

Module C imports A, and B and calls foo() with an int argument:

import A;
import B;
...
foo(3);

Now, the designer of B, having no knowledge of A or that C imports A and B, adds the following declaration to B:

void foo(int x);

Suddenly, C.foo(3) is calling B.foo(3) because B.foo(int) is a better match for 3 than A.foo(long). B.foo(int) is said to have hijacked the call to A.foo(long). In D, such overloading across modules would generate a compile time error if a call from C matches functions from more than one import. Overloading across imports must be done intentionally, not by default, by using an alias declaration:

import A;
import B;
alias A.foo foo;
alias B.foo foo;
...
foo(3);  // calls B.foo(int)

There’s a lot more to function hijacking, see Hijack.

It’s a worthy goal of language design to be able to prevent changes in declarations in one module from producing unexpected bad behavior in another. While I know of no way to eliminate all such cases, D makes some major steps in closing common loopholes.

If you want to learn more about how real compilers work, I am hosting a seminar in the fall on compiler construction.

Home | Runtime Library | IDDE Reference | STL | Search | Download | Forums