strbuf_format(
&buf, "Hello, {1}! 2 + 5 = {0} {{brace}}\n",
2 + 5, "World");
strbuf_format(&buf, "Number = {:#010x}\n", 0xfeed);
strbuf_format(&buf, "Grouped = {:,}\n", 314159265358979323);
This works. It is type-safe. I have not released it because I think it’s a bit… well, painful to use. I’m sure my implementation is a bit different from the one posted, but the key insights are:1. You can write a variadic macro that figures out how many arguments it was passed in __VA_ARGS__,
2. You can use this variadic macro to paste the number of arguments into the name of another macro which is invoked using the ## preprocessor concatenation operator,
3. You can dispatch on the argument types using _Generic.
The limitations are:
- You have to pick a maximum number of arguments and hard-code that maximum into your library (in order for #1 to work above).
- Macro-expansion makes the error messages a bit crazy and hard to read, like C++, but worse.
- _Generic is rather user-hostile when there is an error.
- Partitioning integral types with _Generic is probably the right way to do things. Unlike C++, there is no implicit casting. It is not obvious how to partition the integral types. I ended up using char, short, int, long, and long long, plus unsigned versions of each, and finally signed char (because char != signed char).
I will say that my version is probably a little more efficient at the call site, since it packs the argument types as bits into integer arguments. For example, it’s something like this:
format("x = {} + {}", 2, 3u);
// Turns into something like…
format_impl_("x = {} + {}",
// 2 = count
2 | (kFormatInt << 8) | (kFormatUnsigned << 12),
2, 3u);
So the type information for up to 14 arguments is packed into one additional argument, on 64-bit systems. I use code generation for the macros to handle as many arguments as I like, and there is also 32-bit support.