cd /news/developer-tools/generic-containers-in-c-result · home › topics › developer-tools › article
[ARTICLE · art-157156] src=uecker.codeberg.page ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Generic Containers in C: Result

Martin Uecker published a design for a generic `result` type in C on 2026-10-24, built as a macro wrapping an algebraic `sum` type that pairs a value with an `error_t` string and carries the `[[nodiscard]]` attribute so compilers warn when a returned result is ignored. The design, part of Uecker's noplate library, provides `result_fail` and `result_success` constructors plus `result_ok`, `result_value` and `result_error` accessors, and Uecker notes the macro-based generics require type arguments to be typedef names. Uecker also observed that Anthropic uses sanitizers to verify generated RISC-V C code, an approach similar to his own work on proving safety properties at compile time with GCC's optimizer and sanitizers.

read5 min views1 publishedOct 25, 2026

Martin Uecker, 2026-10-24

Previously, I discussed a [span type](./2025-07-02.html), 
[a vector type](./2025-07-20.html), an optional type
called [maybe](./2025-08-10.html), and a variant type
[variadic](./2025-09-21.html) (now renamed to
`variant` which makes more sense). I experiment with these types in my
[noplate library](https://codeberg.org/uecker/noplate/). 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](https://uecker.codeberg.page/2026-05-22.html).
Interestingly, the idea of using sanitizers for proving safety properties is
also used by Anthropic to [verify](https://www.anthropic.com/research/formal-verification)
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](https://godbolt.org/z/7Y8q8szhP)).
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](https://godbolt.org/z/491ec3n4T)).
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](https://uecker.codeberg.page/2026-05-22.html)
of re-declaring `__builtin_trap` with the `[[warning("UNSAFE")]]`
attribute. It works! Try it yourself on [godbolt](https://godbolt.org/z/rzYGWMoWK)
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)](https://godbolt.org/z/YKfnYdqb7).


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](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3950.pdf)),
but WG14 was not yet convinced.
── more in #developer-tools 4 stories · sorted by recency
── more on @martin uecker 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/generic-containers-i…] indexed:0 read:5min 2026-10-25 · —