Martin Uecker, 2026-10-24
Previously, I discussed a span type,
a vector type, an optional type
called maybe, and a variant type
variadic (now renamed to
variant which makes more sense). I experiment with these types in my
noplate library. A couple
of months ago, I explored how GCC's optimizer in combination with sanitizer can be (mis-)used to prove
safety properties at compile-time.
Interestingly, the idea of using sanitizers for proving safety properties is
also used by Anthropic to verify
C code, although there it is applied to the generated RISC-V code.
Today, I want to discuss the design of a generic result type.
We want a type that behaves similar to maybe, but which can
not be ignored without a warning and which also encodes an error message.
static result(int) divide(int a, int b)
{
if (b == -1 && a == INT_MIN)
return result_fail(int, "overflow in division");
if (b == 0)
return result_fail(int, "division by zero");
return result_success(int, a / b);
}
We start by defining the result type as a generic macro that simply wraps
an algebraic sum type that contains the type of the result
and an error type that simply wraps a string.
typedef struct { const char *error; } error_t;
#define result(T) struct [[nodiscard]] result_##T { sum(T, error_t) sum; }
Here, we used the [[nodiscard]] attribute that tells the
compiler to emit a warning when the result is ignored when returned
from a function.
Side note: A known limitation of our macro-based generics is that the
type arguments must be typedef names, but this is only a minor
inconvenience which hopefully can be addressed in the future. I still
like to make the following observation at this point. An alternative design that
uses tagless structures can use arbitrary type names, because it does not need
to construct a new tag using token concatenation (##). Such types
then always require a single unique declaration before use. But this has a major problem:
Where do you place the declaration for result(T) and
sum(T, error_T) for various types T? These generic
types should be vocabulary types that can freely be used anywhere.
Let's continue constructing result by adding two constructors.
#define result_fail(T, e) (result(T)){ sum_init(T, error_t, (error_t){ (e) }) }
#define result_success(T, v) (result(T)){ sum_init(T, error_t, (v)) }
And finally also some accessor functions.
#define result_ok(T, r) (!(r).sum.sel)
#define result_value(T, r) sum_car(r.sum)
#define result_error(T, r) sum_cdr(r.sum)
The sum type is simply a tagged union which works similar as
maybe as it traps when the inactive member is accessed.
I will write about it another time, as the design of the algebraic
types is not yet completed.
Now let's look how it works in an example (godbolt).
int main()
{
int a = 2;
int b = 1;
divide(a, b);
}
: In function 'main':
:34:5: warning: ignoring return value of type 'struct result_int', declared with attribute 'nodiscard' [-Wunused-result]
34 | divide(a, b);
| ^~~~~~~~~~~~~~~~~
:17:20: note: in call to 'divide', declared here
17 | static result(int) divide(int a, int b)
If we ignore the return value, we get the expected warning. Of course,
divide is a poor example for this, because ignoring the result
is not a problem. For certain other functions this is very useful.
Now let's consider a complete example (godbolt).
int main()
{
int a = 2;
int b = 1;
result(int) r = divide(a, b);
if (result_ok(int, r)) {
printf("The result is %d.\n", result_value(int, r));
} else {
puts(result_error(int, r).str);
}
}
But is this now safe? Let's try to use the result value in the false branch.
} else {
printf("The result is %d.\n", result_value(int, r));
puts(result_error(int, r).str);
}
If you also change b to zero, this will trigger a run-time error.
But what if I want to avoid run-time errors?
Let's try our previously described trick
of re-declaring __builtin_trap with the [[warning("UNSAFE")]]
attribute. It works! Try it yourself on godbolt
by changing the #if 0 to #if 1. This works because because noplate
internally also uses __builtin_trap>.
There is still a minor potential issue with the design of the type.
If we add [[gnu::noinline,gnu::noipa]]
to the divide function, so that the compiler will not look into
it, it will then complain that the argument to puts maybe null.
This is actually nice, as it tells us that there might be a run-time issue!
Note, that in this example, without the attributes, it can see that the pointers
to the error strings are not null, but when the divide function were
moved into a library, the information about the pointers would be inaccessible to the optimizer.
To avoid this, we could modify the error_t type to ensure that the error
string is always initialized in a way the optimizer can see this. For example, a version which uses
the strv (string view) type from the noplate library instead of a traditional
C string does not have this problem (the noplate's README contains a link to such an example).
For now, we can just add a check for this. The complete example, that the compiler now
has proven statically safe with regard to signed integer operations, null pointers,
and error handling can be found here (godbolt).
Of course, the compile-time check using GCC's optimizer is hack, but it shows what can be
done relatively easily with existing technology already shipped with compilers.
We only need a better interface to it. There is a proposal to add a compiler_assert
feature to C (PDF),
but WG14 was not yet convinced.