Question
What Does `:-!!` Mean in C? Bit-Fields, Compile-Time Errors, and Linux Macros
Question
I came across this macro in Linux header code and want to understand the expression :-!!(e):
/* 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); }))
#define BUILD_BUG_ON_NULL(e) ((void *)sizeof(struct { int:-!!(e); }))
What does :-!! mean in this context?
Note: in newer Linux versions, this macro has been moved to /usr/include/linux/build_bug.h.
Short Answer
By the end of this page, you will understand how :-!!(e) works in C, why it appears inside Linux kernel macros, and how it uses a bit-field with an invalid width to deliberately trigger a compile-time error when a condition is true.
Concept
In this macro, :-!!(e) is not a single C operator. It is a combination of two separate pieces of C syntax:
:— the separator used in a bit-field declaration inside astruct!!(e)— a common C idiom that converts any non-zero value to1and zero to0
So this code:
struct { int:-!!(e); }
is declaring an unnamed bit-field of type int with width -!!(e).
Why does that matter?
In C, a bit-field width must be a non-negative integer constant expression in valid use cases. If the width becomes negative, the compiler rejects it.
Now look at the logic:
- If
eis0, then!!(e)becomes0, so-!!(e)becomes0 - If
eis non-zero, then!!(e)becomes1, so-!!(e)becomes-1
That means:
e == 0→ bit-field width is0→ valid enough for this macro patterne != 0→ bit-field width is-1→ invalid → compile-time error
This is a clever trick used to make the compiler fail during compilation, not at runtime.
Why Linux uses this
The Linux kernel often needs to verify assumptions at compile time, such as:
- structure sizes
- alignment requirements
- constant relationships
- architecture-specific constraints
If such an assumption is wrong, the code should not compile at all. Macros like BUILD_BUG_ON_ZERO(e) exist for exactly that purpose.
Why wrap it in sizeof
The macro is written like this:
sizeof(struct { int:-!!(e); })
Using sizeof gives an expression result without creating an actual object. It also lets the macro be used in places where a normal statement would not be allowed, such as inside initializers.
The important idea is:
- valid condition → expression compiles and produces a value
- invalid condition → compiler errors out immediately
This is an old and very practical metaprogramming technique in C before more modern compile-time checking tools became common.
Mental Model
Think of this like a security gate in the compiler.
!!(e)turns the condition into a clean yes/no value:- no →
0 - yes →
1
- no →
- the leading minus sign flips that into:
- no →
0 - yes →
-1
- no →
Then the bit-field width acts like a rule:
- width
0→ allowed - width
-1→ forbidden
So the compiler is being asked:
“Create a tiny struct with a field width of either
0or-1.”
If the answer is -1, the compiler refuses. That refusal is the whole point.
Another way to think about it:
!!(e)is a boolean normalizer-turns true into an illegal width- the bit-field declaration turns that illegal value into a compile-time failure
Syntax and Examples
The key syntax
struct {
int : width;
}
This declares an unnamed bit-field with the given width.
In the Linux macro, the width is computed:
int : -!!(e);
Understanding !!
int a = !!0; // 0
int b = !!5; // 1
int c = !!-10; // 1
!!x is a common C trick:
- first
!xconverts zero to1, non-zero to0 - second
!flips that again - result is normalized to
0or1
Small example
#include <stdio.h>
#define CHECK_ZERO(e) sizeof(struct { int : -!!(e); })
int main(void) {
size_t ok = CHECK_ZERO(0);
printf("ok = %zu\n", ok);
return 0;
}
This compiles because:
eis0!!(0)is0-0is0
So the bit-field width is 0.
Example that fails
#define CHECK_ZERO(e) sizeof(struct { int : -!!(e); })
int x = CHECK_ZERO(123);
This fails because:
!!(123)is1-1is invalid as a bit-field width
Why not just use if?
Because if is runtime logic. This macro is for conditions that should be rejected before the program can even be built.
Step by Step Execution
Consider this example:
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int : -!!(e); }))
size_t value = BUILD_BUG_ON_ZERO(4);
Step 1: Substitute the macro
The preprocessor expands it to:
size_t value = sizeof(struct { int : -!!(4); });
Step 2: Evaluate !!(4)
!(4) // 0
!!(4) // 1
So now we have:
size_t value = sizeof(struct { int : -1; });
Step 3: Parse as a bit-field
Inside the struct, this means:
- unnamed bit-field
- type
int - width
-1
Step 4: Compiler validates the bit-field width
A negative bit-field width is invalid, so compilation fails.
Now compare with:
size_t value = BUILD_BUG_ON_ZERO(0);
Step 1: Expand the macro
size_t value = sizeof(struct { int : -!!(0); });
Step 2: Evaluate !!(0)
!(0) // 1
!!(0) // 0
So this becomes:
size_t value = sizeof(struct { int : 0; });
Step 3: Validate width
Width 0 is acceptable for this pattern, so the expression compiles.
Step 4: sizeof(...) produces a value
The exact size is not the main point here. The real purpose is that the expression exists and compilation succeeds only when the condition is false.
Real World Use Cases
Compile-time error macros like this are used when a broken assumption must stop the build immediately.
Common situations
-
Structure layout checks
- Ensure a struct has the expected size
- Useful in kernels, drivers, embedded code, and binary protocols
-
Alignment validation
- Check that offsets or sizes are multiples of a required alignment
-
Array and buffer constraints
- Reject code if a fixed buffer size is too small
-
Platform-specific assumptions
- Verify integer widths, pointer sizes, or hardware constants
-
Constant configuration checks
- Ensure one compile-time constant is not larger than another
Example
#define MUST_BE_8_BYTES(type) \
((void)sizeof(struct { int : -!!(sizeof(type) != 8); }))
This could be used to ensure a type has exactly 8 bytes.
Why this matters
In low-level code, runtime checks may be too late. If memory layout is wrong, the program may already be unsafe. A compile-time failure is much better.
Real Codebase Usage
In real projects, developers use compile-time checks to protect assumptions that should never be violated.
Common patterns
Guarding invariants
#define STATIC_CHECK(cond) sizeof(struct { int : -!!(cond); })
Used when a condition must remain false.
Validating constants in headers
Header files often need checks that work in expression contexts, not just statements.
int table[10 + STATIC_CHECK(MY_LIMIT > 10)];
Defensive macro design
Linux-style macros often try to:
- fail early
- work in initializers or declarations
- avoid generating runtime code
Modern replacements
In newer code, developers may prefer:
_Static_assertin C11+static_assertin C++
Example:
_Static_assert(sizeof(long) == 8, "long must be 8 bytes");
This is clearer than the bit-field trick, but older code and portability concerns explain why the macro pattern exists.
Related real-world practices
- validating ABI compatibility
- checking protocol header sizes
- preventing invalid configuration combinations
- asserting assumptions in embedded firmware
Common Mistakes
1. Thinking :-!! is a special operator
It is not.
This is wrong thinking:
// There is no C operator named :-!!
What is really happening:
:belongs to bit-field syntax-is unary minus!!converts a value to0or1
2. Forgetting that this is inside a struct field declaration
This only makes sense in a bit-field context.
struct {
int : -!!(e);
};
Outside a bit-field declaration, the colon would not mean the same thing.
3. Assuming it runs at runtime
This is not runtime logic.
Broken mental model:
if (e) {
// maybe it crashes later
}
Actual behavior:
- the compiler checks the declaration while compiling
- if invalid, build fails before the program runs
4. Using non-constant expressions where compile-time checks are expected
Many compile-time tricks only work correctly when the condition can be evaluated at compile time.
Example:
int x = 5;
size_t y = sizeof(struct { int : -!!(x); });
This may not behave as intended because bit-field widths are expected to be integer constant expressions.
5. Confusing !x with !!x
!5 // 0
!!5 // 1
!xmeans logical NOT!!xmeans normalize to boolean-like0or1
6. Reusing this trick in modern C when _Static_assert is available
This older pattern is clever, but often less readable.
Prefer this when possible:
_Static_assert(sizeof(int) == 4, "int must be 4 bytes");
Use the old macro style only when you specifically need expression-context behavior or compatibility with older code.
Comparisons
Compile-time tricks compared
| Technique | Purpose | Works in expression context? | Readability | Typical use |
|---|---|---|---|---|
| Bit-field width trick | Force compile-time error | Yes | Low | Older C macros, kernel-style code |
sizeof(char[(cond) ? 1 : -1]) | Force compile-time error | Yes | Medium | Older C compile-time assertions |
_Static_assert(cond, msg) | Compile-time assertion | No, it is a declaration | High | Modern C11+ code |
if (...) | Runtime branching | No | High | Runtime validation |
!x vs !!x
| Expression | If x == 0 | If x != 0 |
|---|---|---|
!x | 1 | 0 |
!!x | 0 | 1 |
-!!x | 0 | -1 |
Bit-field trick vs _Static_assert
#define OLD_CHECK(e) sizeof(struct { int : -!!(e); })
_Static_assert(sizeof(long) == 8, "long must be 8 bytes");
When to use which
- Use
_Static_assertfor clarity in modern C - Use the older trick only when you need:
- compatibility with older compilers
- expression-context usage
- kernel-style macro patterns
Cheat Sheet
Quick meaning
int : -!!(e)
means:
!!(e)→ converteto0or1-!!(e)→ becomes0or-1- used as a bit-field width
- width
-1causes a compile-time error
Expansion logic
e | !!(e) | -!!(e) | Result |
|---|---|---|---|
0 | 0 | 0 | compiles |
| non-zero | 1 | -1 | compile-time error |
Core pattern
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int : -!!(e); }))
Why sizeof is used
- produces an expression value
- avoids runtime code
- allows use in more places than a statement macro
Related modern syntax
_Static_assert(condition, "message");
Important facts
:-!!is not one operator:is from bit-field syntax!!xis a boolean-normalizing idiom- negative bit-field width is invalid
- this is a compile-time metaprogramming trick
FAQ
What does :-!! mean in C?
It is not a single operator. It is a bit-field colon : followed by the expression -!!(e), which evaluates to 0 or -1.
Why does !!(e) appear in C code?
It converts any non-zero value to 1 and zero to 0. This is a common C idiom when code needs a clean boolean-like integer.
Why does a negative bit-field width cause an error?
Because bit-field widths must be valid non-negative widths in this usage. A negative width is invalid, so the compiler rejects the code.
What is BUILD_BUG_ON_ZERO used for?
It is used to force a compile-time failure when a condition is true, while still behaving like an expression.
Is this still used in modern C?
Older codebases and low-level projects still use patterns like this, but modern C usually prefers _Static_assert when possible.
Is :-!! Linux-specific syntax?
No. The syntax is standard C components used in a clever way. The macro itself is common in Linux-style low-level C code.
Can I replace this with _Static_assert?
Often yes, if your compiler supports C11 or later and you do not need expression-context behavior.
Why not just use assert()?
assert() is a runtime check. BUILD_BUG_ON_ZERO is a compile-time check.
Mini Project
Description
Build a small compile-time validation header that checks assumptions about constants and type sizes. This demonstrates how expression-based compile-time checks work and helps you compare the older bit-field trick with clearer modern alternatives.
Goal
Create a C program that compiles only when a few predefined conditions are valid, and fails to compile when they are not.
Requirements
- Define a macro that uses the bit-field-width trick to trigger a compile-time error.
- Use the macro in at least two checks involving constants or
sizeof. - Print a success message only if the program compiles.
- Include one commented example that would intentionally fail if uncommented.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.