Comment Macros — Executable Documentation
Needful provides a family of macros that replace prose comments with compile-checked expressions. In C, they are all no-ops. In C++ builds, they verify that the expression inside is well-formed — so if you rename a variable, the comment breaks at compile time instead of silently going stale.
The Problem With Prose Comments
int i = Get_Integer(...); // i may be < 0
If i is later renamed, the comment becomes wrong silently. The expression i < 0 is not checked by the compiler.
The Needful Alternative
int i = Get_Integer(...);
possibly(i < 0); // if i is renamed, this becomes a compile error
possibly() is a no-op at runtime — it compiles away completely. But in C++ builds, it static-asserts that the expression has a type convertible to bool. If i is renamed and the expression no longer compiles, CI catches it.
Start Here
If you are evaluating Needful, this is the page to try first.
Everything else in this library has a peer somewhere. Rust has Option and Result, C++17 has [[nodiscard]], and every large C codebase eventually grows its own cast wrappers. Comments that fail the build when they go stale have no equivalent in any of them.
They are also the cheapest thing here to adopt. Every other construct asks you to change a type signature and think about who owns what:
| asks you to | breaks if you stop | |
|---|---|---|
Option(T), Need(T), Fallible(T) | change declarations, add unwrap/opt | yes — signatures changed |
Result(T) | supply failure hooks, restructure returns | yes |
| Comment macros | nothing | no — no declaration ever changed |
You can add possibly() to one function this afternoon, in an existing file, with no refactoring and no build changes, and roll the whole experiment back by deleting the lines. (The one exception is heeded(), which really does evaluate its expression — see the table below.) That makes them a good place to start, and in practice the thing that gets people to try Option(T) a month later.
Statement-Scope Macros
These go inside function bodies:
| Macro | Meaning | Constraint |
|---|---|---|
possibly(cond) | This might be true | cond must be bool-convertible |
definitely(cond) | This is always true (not worth asserting at runtime) | cond must be bool-convertible |
impossible(cond) | This can never be true | cond must be bool-convertible |
unnecessary(expr) | This code would be redundant or pointless here | expr must be valid |
inapplicable(expr) | This operation does not apply in this case | expr must be valid |
dont(expr) | You might think you need to do this, but it’s wrong! | expr must be valid |
cant(expr) | Would like to do this; current limitations prevent it | expr must be valid |
heeded(expr) | This looks stray but its side effect is intentional | expression is evaluated |
Note that heeded() is the one entry that is not a no-op: it expands to USED(expr), so the expression really runs. The others compile away.
Global-Scope Macros
For use outside function bodies (at file or namespace scope). They are uppercased and have slightly different expansion:
| Macro | Meaning |
|---|---|
POSSIBLY(cond) | No-op at global scope |
DEFINITELY(cond) | Static assertion that cond is true |
IMPOSSIBLE(cond) | Static assertion that cond is false |
UNNECESSARY(expr) | No-op at global scope |
DONT(expr) | No-op at global scope |
CANT(expr) | No-op at global scope |
STATIC_ASSERT and Friends
STATIC_ASSERT(sizeof(int) == 4); // C++ build: compile error if false
STATIC_ASSERT_LVALUE(variable); // error if variable is not an lvalue
STATIC_IGNORE(expr); // validate expression, discard result
STATIC_FAIL("path is unreachable"); // always fails (marks unreachable paths)
STATIC_FAIL() takes a string literal in every build mode. Pre-C11 C has no _Static_assert, so it falls back to a negative-size array typedef: still a hard error, but the message itself is lost, since there is nowhere to put it.
STATIC_ASSERT() is enforced in C++ builds and in C11 or later. In pre-C11 C it is a no-op, so do not rely on it as your only check of an invariant that matters.
Example: Self-Documenting Loop
uint32_t calculate_sum_weirdly(uint8_t* arr, int len) {
uint32_t sum = 0;
while (len > 0) {
possibly(arr[len - 1] == 0); // some elements may not change sum
sum += arr[--len];
}
definitely(len == 0); // we decremented past 0
return sum;
}
Related
Compile-Time Tests
Comment macros compile and are no-ops at runtime
#include <assert.h>
#ifdef __cplusplus
#define NEEDFUL_CPP_ENHANCED 1
#endif
#define NEEDFUL_COMMENT_SHORTHANDS 1
#include "needful.h"
int find_first_nonzero(int* arr, int len) {
for (int i = 0; i < len; ++i) {
possibly(arr[i] == 0); // some elements may be zero
if (arr[i] != 0)
return arr[i];
}
impossible(len > 0); // only reached when all are zero
return 0;
}
int main() {
int a[] = {0, 0, 7, 3};
assert(find_first_nonzero(a, 4) == 7);
int b[] = {0, 0};
assert(find_first_nonzero(b, 2) == 0);
return 0;
}
possibly() requires a bool-convertible expression
// MATCH-ERROR-TEXT: must be explicitly convertible to bool <- needful static_assert
// MATCH-ERROR-TEXT: static assertion failed <- GCC/Clang
#ifdef __cplusplus
#define NEEDFUL_CPP_ENHANCED 1
#endif
#define NEEDFUL_COMMENT_SHORTHANDS 1
#include "needful.h"
struct NotBool { int x; };
int main() {
NotBool nb = {5};
possibly(nb); // ERROR: NotBool has no operator bool
return 0;
}