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.
Final Switch
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.
Override
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.
Function Hijacking
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.
Conclusion
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.