My post on Delphi intrinsic functions that evaluate to consts as a step up to Delphi compile time assertions.
This is a corner case of Delphi language use, which can come in very handy when your code is changed in the future, and you want to be prepared to ensure that some changes do not violate some predefined boundaries.
Hopefully a future post will elaborate a bit more on actual usage, but for now, lets first show some examples, then some other languages that have a richer set of compile time assertions.
My original goal was to see if I could come up with a mechanism that allowed for better validation of generic types because Delphi generic constraints – still – are quite limited: Delphi Constraints in Generics – RAD Studio XE documentation wiki, so limiting or verifying the aspects of the concrete type often cannot be done by constraints.
C# had a similar limitation for constraining to enum, which finally got added some 13 years after adding generics, in 2018: [WayBack] Unmanaged, delegate and enum type constraints – C# 7.3 in Rider and ReSharper – .NET Tools Blog.NET Tools Blog.
Let’s start simple:
const
// forbidden const values to check compile time assert:
A = 0;
B = 1;
C = -1;
// The below expressions all each generate a "[dcc32 Error] E2098 Division by zero" (so multiple errors in one compile)
// Asserting at compile time using boolean expressions:
BooleanAssertAIsNotZero = 1 div Ord(A <> 0);
BooleanAssertBIsNotOne = 1 div Ord(B <> 1);
// Asserting at compile time using numeric expressions:
AssertAIsNotZero = 1 div A;
AssertBIsNotOne = 1 div (B - 1);
AssertBIsNotAbsOne = 1 div (Abs(B) - 1);
AssertCIsNotAbsOne = 1 div (Abs(C) - 1);
This is all centered around generating a compile time error "[dcc32 Error] E2098 Division by zero"
, of which multiple can occur in one compile go (after compilation, the cursor focus will be at the first error) and which has been in the language for a very long time [WayBack] E2098: Division by zero.
The conversion of Boolean
to Integer
is done using Ord
, a very powerful compile time intrinsic that evaluates to a constant.
You can use this for other intrinsics as well, for example:
type
TDigits = 0..9;
const
DigitsAreInteger = GetTypeKind(TDigits) = tkInteger;
DigitsAreIntegerIsTrue = 1 div Ord(DigitsAreInteger);
DigitsAreEnumeration = GetTypeKind(TDigits) = tkEnumeration; // compiles fine
DigitsAreEnumerationIsTrue = 1 div Ord(DigitsAreEnumeration); // [dcc32 Error] E2098 Division by zero
The above learns that integer subranges are not enumerations, but stay integers.
You can now extend this to check longer boolean expressions, for instance to check if a record size matches certain criteria. For this we create records having zero to four bytes in size (yes, you can have empty record in Delphi, it in fact the only data structure that can be zero bytes in length, though the documentation [WayBack] Structured Types: record types does not state this is in fact possible ), then validate the sizes:
type
TRecord0 = record
end;
TRecord1 = packed record
FByte0: Byte;
end;
TRecord2 = packed record
FByte0: Byte;
FByte1: Byte;
end;
TRecord3 = packed record
FByte0: Byte;
FByte1: Byte;
FByte2: Byte;
end;
TRecord4 = packed record
FByte0: Byte;
FByte1: Byte;
FByte2: Byte;
FByte3: Byte;
end;
const
AssertTRecord0SizeOf0 = 1 div Ord(SizeOf(TRecord0) = 0); // When expression is false: [dcc32 Error] E2098 Division by zero
AssertTRecord0SizeOf1 = 1 div Ord(SizeOf(TRecord1) = 1); // When expression is false: [dcc32 Error] E2098 Division by zero
AssertTRecord0SizeOf2 = 1 div Ord(SizeOf(TRecord2) = 2); // When expression is false: [dcc32 Error] E2098 Division by zero
AssertTRecord0SizeOf3 = 1 div Ord(SizeOf(TRecord3) = 3); // When expression is false: [dcc32 Error] E2098 Division by zero
AssertTRecord0SizeOf4 = 1 div Ord(SizeOf(TRecord4) = 4); // When expression is false: [dcc32 Error] E2098 Division by zero
AssertTRecord0SizeOfMultipleOf4 = 1 div Ord(SizeOf(TRecord0) mod 4 = 0); // When expression is false: [dcc32 Error] E2098 Division by zero
AssertTRecord4SizeOfMultipleOf4 = 1 div Ord(SizeOf(TRecord4) mod 4 = 0); // When expression is false: [dcc32 Error] E2098 Division by zero
AssertTRecord0SizeOfMultipleOf4AndGreaterThan0 = 1 div Ord((SizeOf(TRecord0) mod 4 = 0) and (SizeOf(TRecord0) > 0)); // When expression is false: [dcc32 Error] E2098 Division by zero
AssertTRecord4SizeOfMultipleOf4AndGreaterThan0 = 1 div Ord((SizeOf(TRecord4) mod 4 = 0) and (SizeOf(TRecord4) > 0)); // When expression is false: [dcc32 Error] E2098 Division by zero
That’s how far I got in my first experiments using this mechanism. Hopefully it gave you some inspiration too, so I welcome any usages you made with it.
Inline use of intrinsics can lead to no generated code at all
Since Delphi has no macro language, you cannot create your own intrinsic functions that evaluate to const. You could use a pre-processor though, as described in [WayBack] How to write Delphi compile-time functions – Stack Overflow.
The answer by Johan there however mentions clever use of in-line functions that do not generate any code at all (so effectively evaluate to a const). More on that in a future post.
Compile time assertions in other languages
Many languages support a form of [WayBack] Compile-time calculation – Rosetta Code. If such a language can errors out on compiling such a calculation, then you can have compile time assertions.
Compile time assertions are very much used in C and C++, where they are often called static assertions. Often they depend on macros, but C11 (C standard revision 11) has it built-in.
Since I also do quite a bit of .NET: [WayBack] Can C# Provide a static_assert? – Stack Overflow
Some links on how they work in C and C++, and what you can do with them:
- [WayBack] C compiler asserts – how to implement? – Stack Overflow
The key trick is to find a construct that can be evaluated at compile time and can cause an error for some values. One answer is the declaration of an array cannot have a negative size. Using a typedef prevents the allocation of space on success, and preserves the error on failure.
- [WayBack] static_assert macro – – C – WikiChip
The static_assert object-like macro expands to the _Static_assert, a keyword added in C11 to provide compile-time assertion.
- [WayBack] boost/static_assert.hpp – 1.69.0
- [WayBack] Compile time assertions in C
- [WayBack] Catching errors early with compile-time assertions | Embedded
- [WayBack] c – compile-time struct size check, error out if odd – Stack Overflow
- [WayBack] DCL03-C. Use a static assertion to test the value of a constant expression – SEI CERT C Coding Standard – Confluence
- [WayBack] C/C++ Compile Time Asserts – I know the answer (it’s 42)
- [WayBack] GNU Gnulib: Compile-time Assertions
- [WayBack/Archive.is] Simple and Static Assertion (assert) in C Programming Language
- [WayBack] Static Assertion – cppreference.com
- [WayBack] Different behaviour of C macro for different cases – Stack Overflow: BUILD_BUG_ON
- [WayBack] A compile time assert() hack for C : programming
- [WayBack] linux/build_bug.h at master · torvalds/linux · GitHub:
-
/*
* Force a compilation error if condition is true, but also produce a
* result (of value 0 and type size_t), so the expression can be used
* e.g. in a structure initializer (or where-ever else comma expressions
* aren't permitted).
*/
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:(-!!(e)); }))
-
/* Force a compilation error if a constant expression is not a power of 2 */
#define __BUILD_BUG_ON_NOT_POWER_OF_2(n) \
BUILD_BUG_ON(((n) & ((n) - 1)) != 0)
#define BUILD_BUG_ON_NOT_POWER_OF_2(n) \
BUILD_BUG_ON((n) == 0 || (((n) & ((n) - 1)) != 0))
-
/*
* BUILD_BUG_ON_INVALID() permits the compiler to check the validity of the
* expression but avoids the generation of any code, even if that expression
* has side-effects.
*/
#define BUILD_BUG_ON_INVALID(e) ((void)(sizeof((__force long)(e))))
-
/**
* BUILD_BUG_ON_MSG - break compile if a condition is true & emit supplied
* error message.
* @condition: the condition which the compiler should know is false.
*
* See BUILD_BUG_ON for description.
*/
#define BUILD_BUG_ON_MSG(cond, msg) compiletime_assert(!(cond), msg)
-
/**
* BUILD_BUG_ON - break compile if a condition is true.
* @condition: the condition which the compiler should know is false.
*
* If you have some code which relies on certain constants being equal, or
* some other compile-time-evaluated condition, you should use BUILD_BUG_ON to
* detect if someone changes it.
*/
#define BUILD_BUG_ON(condition) \
BUILD_BUG_ON_MSG(condition, "BUILD_BUG_ON failed: " #condition)
-
/**
* BUILD_BUG - break compile if used.
*
* If you have some code that you expect the compiler to eliminate at
* build time, you should use BUILD_BUG to detect if it is
* unexpectedly used.
*/
#define BUILD_BUG() BUILD_BUG_ON_MSG(1, "BUILD_BUG failed")
–jeroen
Like this:
Like Loading...