I start with the basics and implement everything as explicitly (read: type safe) as possible:
struct bob_params {...}
struct line_params {...}
pdf_page_cmd_bob(bob_params *params) { ... }
pdf_page_cmd_line(line_params *params) { ... }
Then, when that works and I want to add patterns, which are a superset, I'd add something that is literally a superset of those functions. enum pattern_type {
patterntype_Bob,
patterntype_Line,
}
struct pattern {
pattern_type type;
union {
bob_params Bob;
line_params Line;
}
}
pdf_pattern_cmd(pattern *pattern) {
switch (pattern->type) {
case patterntype_Bob: /* do bob drawing a bunch of times, or whatever */
}
}
// The pattern struct has a lot of different names (sum type, discriminated union, algebraic datatype, tagged union .. probably more), but the idea is the 'type' tag tells you which one of the union values to use.
I've tried everything and, as far as I can tell, this gets you the best of all worlds. It's pretty much as type-safe as things get in C. It's extremely flexible; you just mix the fundamentals together as you like when adding higher-order functions. It's fast; the compiler can see exactly what's going on, so you pay minimal runtime cost.Yes, it's a bunch of typing to get the functions all spelled out, but it's really not that bad considering how easy/obvious the code actually is.
I use this pattern so much I actually wrote a little metaprogramming language that is capable of generating a lot of the boilerplate for you. Link in my bio, if anyone's interested in looking at it.