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.
Perform the following steps:
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.");
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.
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); }
18.221.249.198