Using static asserts

A static assert is the main language feature for implementing custom compile-time checks. It can check any condition. If the static assert is compiled, the condition must be true; otherwise, it will issue a compile-time error. Here, we'll use it to verify whether our struct is of the correct size to interface with hardware.

How to do it…

Perform the following steps:

  1. Write static assert(condition, message); in the code.
  2. If your condition is too complex to represent as a simple condition, you may write a helper function and call it for the condition.
  3. Remember that static assert must pass if the code is compiled, even if the path is not actually executed.

The code is as follows:

align(1) struct IDTLocation {
  align(1):
  ushort length;
  uint offset;
}
static assert(IDTLocation.sizeof == 6, "An IDTLocation must be exactly six bytes with no padding.");

How it works…

The compile-time partner to assert is static assert. It functions in the same way, but instead of checking a runtime execution path, it checks a compile-time path. Whenever a static assert is compiled (specifically, this check occurs during the semantic analysis phase of compilation), its condition must pass or else a compile error will be triggered.

Compile errors do not necessarily cause the build to fail. Whether something compiles or not can be checked by the program. In these conditions, a compile failure is delivered to the program rather than being displayed to the user. Also, if a template fails to instantiate, the compiler may just use an alternate path, hiding the error. For example, if you are writing a wrapped type with overloaded operators and alias this and have a compile error inside the overloaded operator. Then, the compiler will see it as unsuitable and fall back on alias this, silently hiding your mistake! If you are in doubt that your code path is being attempted to compile, you can double-check by adding pragma(msg, "any message here"); somewhere near your static assert.

Your compile-time assumptions should be checked by static assert, especially when interfacing with hardware or outside programs when binary layouts must be precisely correct to avoid memory corruption and other crashes. In our preceding example, we used it to verify the size of an IDTLocation struct that is loaded by the processor so that it can find interrupt handlers. If this is not loaded correctly, the CPU will almost immediately triple-fault when it tries to run the program, causing the computer to restart! These low-level bugs can be difficult to trace down and if driving real hardware, it could be physically damaging.

It is considerably cheaper and easier to double-check your assumptions at compile time. Even though this data structure looks simple enough to verify by simply looking at it, hidden issues such as data padding and alignment may catch you by surprise. If you remove align(1) from this structure, you'll see the assertion will fail because the compiler will align struct data members on native machine word boundaries by default. In this case, it would add an additional 2 bytes of padding.

One similar problem that can be caught by static assert is comparing struct sizes for C interoperation across platforms. For example, the Linux xlib library uses structs with C's long type to represent GUI events. C's long is not the same as D's long. In D, long is always a 64-bit integer. In C, long is 32 bits on all 32-bit platforms as well as 64-bit Windows; however, on 64-bit Linux, C's long is itself a 64 bit type.

When writing C bindings, binary compatibility is a must. A simple copy-and-paste of the C code will work on 64-bit Linux, but will crash on 32-bit systems! If we use a static assert to verify the size matches the C compiler's requirements (write a small program that simply prints sizeof(The_Type) and be sure to build and run it on the same platform to get the correct expected value). Our mistake will be caught at compile time instead of causing a crash. This may feel like tedium when checking it, but believe me, it will all be worth it the first time it catches a mistake.

Tip

When writing bindings to C libraries, import core.stdc.config; and use the types c_long and c_ulong to do the correct translation. However, while this issue can be fairly easily fixed, the static assert still serves as a valuable sanity check to ensure you did do this correctly and to be a first line of defense against other translation mistakes.

Checking structure sizes is not the only thing done by static assert. It can check anything that can be run at compile time. We've used it before to verify our range objects adhere to a particular compile-time interface. Phobos' std.random also uses it to ensure parameters to a random number engine are good for pseudorandom number generation by checking the greatest common denominator and number of factors the integer parameters have.

It achieves these checks by simply writing regular functions to check the numbers and calling them in a series of static assert statements in the object as shown as follows:

struct LinearCongruentialEngine(UIntType, UIntType a, UIntType c, UIntType m) {
  private static bool properLinearCongruentialParameters(
  ulong m,  ulong a, ulong c)
  {
    /* snip some implementation */
    if (a == 0 || a >= m || c >= m) return false;
    // c and m are relatively prime
    if (c > 0 && gcd(c, m) != 1) return false;
    // a - 1 is divisible by all prime factors of m
    if ((a - 1) % primeFactorsOnly(m)) return false;
    // if a - 1 is multiple of 4, then m is a multiple of 4 too.
    if ((a - 1) % 4 == 0 && m % 4) return false;
    // Passed all tests
    return true;
  }
  static assert(c == 0 || properLinearCongruentialParameters(m, a, c), "Incorrect instantiation of LinearCongruentialEngine");
  /* Snip remaining object implementation */
}

The one thing to be aware of is that static assertions are run if they are compiled. Even if it is put in a part of a function which is unreachable at run time, it still must pass. Consider the following code snippet:

void foo() {
  return;
  // the following line is unreachable code, but still must
  // compile. Since the condition of zero is always false, this
  // assert will fail, and thus this function will not compile.
  static assert(0);
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.249.198