Generic Containers in C: result

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.