It's a bit like putting the length of an array at the end rather than the beginning though - you need to know its size first to know the offset.
The value of a tagged union may be like a flexible array member - in that the size of the value depends on the tag. We shouldn't need to allocate the maximum possible variant's space when most of the instances may be the smallest. Unlike a C `union`, which are a fixed size, a tagged union has no such requirement - it can be dynamically sized depending on tag.
The only time you can get away with only allocating the 'real' size is if you're sticking it in its own separte memory allocation and using a pointer to refer to it in the aforementioned contexts. Which, to be fair, is a sensible thing to do if BigThing is very big, as opposed to just 'on the bigger end of sane struct sizes'.
To clarify I'm talking about what you'd do in a language implementation where the actual memory layout is opaque to userspace and bit twiddling optimizations aren't making things harder to understand for everything, not what you'd do when writing handwritten C code or an api where you expect people to access the memory directly. This isn't unheard of - e.g. from my research Swift does it, some C++ stdlib implementations do it.
(Related - the rest of the bag of weird tricks that Rust and Swift and presumably other languages use to optimize enum/union layout are pretty interesting - the tag bits actually take up no extra space in many cases).
public sealed abstract class Option<T> {
private Option() { }
public sealed class Some<T> : Option<T> {
private readonly T value;
public Some(T value) {
this.value = value;
}
}
public sealed class None<T> : Option<T> {
public None() { }
}
public static Option<T> Some(T value) { return new Some(T); }
static readonly Option<T> _None = new None();
public static Option<T> None() { return _None; }
}
Where you can use `Option.Some` or `Option.None`, and the return type is inferred from the parameter.The sizeof(None) is not the same as sizeof(Some<T>) - the Some class has an additional field not present in the None class - and the size of Some depends on the type parameter T.
If the objects have RTTI, we don't even need a tag field, as there is an implicit tag in the form of type information in the object - the pattern matching facility can test `opt is Some<T>` or `opt is None`.
In C we would have to use a pointer to option to do the equivalent, eg with some GCC extensions we can do:
enum option_tag { OPT_NONE, OPT_SOME };
typedef struct option { enum_option_tag tag; } *Option;
struct none { struct option base; } _none = { .base.tag = OPT_NONE };
Option None = (Option)&_none;
#define Some(T, _value) \
({ \
struct some_##T { \
struct option base; \
T value; \
} *result = GC_alloc(sizeof(struct some_##T)); \
result->base.tag = OPT_SOME; \
result->value = (_value); \
(struct option*)result; \
})
bool is_some(Option opt) {
return opt->tag == OPT_SOME;
}
bool is_none(Option opt) {
return opt->tag == OPT_NONE;
}
#define _get_some(T, opt) (((struct some_##T*)(opt))->value)
This ONLY works if the tag is the first field - casting between pointers to different types is UB if the first fields are not the same.Not the most efficient way to implement an Option in C, but for other tagged unions, if any of the values are particularly large this may be beneficial.
Unless, you do de-serialization and serialization in tight-loop I guess is it acceptable.
NOTE: A bigger problem is when you need to decide at runtime which one of the kind you have been handled.