Metaprogramming custom control structures in C (2012)
chiark.greenend.org.uk
chiark.greenend.org.uk
It's functionally blackbox compatible with the STL for the major containers. Unless you're writing heap spaced firmware where C++ with an STL isn't available I recommend you just use modern day C++.
Unless, of course, you want blazing fast compile times!
CTL
#define P
#define T int
#include <vec.h>
STL #include <vector>
template class std::vector<int>;
C++ with its STL is just dramatically slower at compiling it all. C loves to chew through its basic syntax and O(1) lookup since every symbol is essentially unique (take that C++ and your function overloading!)1. Operating on intrusive containers requires no heap operations, all control structures are preallocated. This is golden, and in more ways than one.
2. Keeping an item in multiple containers has the exact same semantics as storing it in just one. With STL it requires switching from storing items to storing pointers to them. Meaning that you can't throw foo from an existing list into some extra map without reworking all list-related code.
I get the advantages in abstract, and intrusive containers are common in kernel programming where allocation is strictly controlled and objects have limited membership, I'm just curious about the applicability to more general programming and domain modelling, and whether it scales in terms of developer productivity.
For instance, how common are programs that store objects in multiple dictionaries and/or multiple lists simultaneously? You say this has the same semantics as storing it one container, but I'm not clear what you mean by this.
Also, if you want to extend an object's membership to another container, what sorts of changes are required compared to non-intrusive containers [1]? Adding an object to a non-intrusive container is a simple local change, ie. container.Add(item), but with an intrusive container you need to actually extend the type definition itself, an intrusive non-local change; this should inhibit some forms of extension, so I want a better understanding of that impact.
Finally, do intrusive containers retain meaningful advantages in languages with garbage collection? Certainly less allocation is one obvious benefit, but are there downsides? Functional languages in particular emphasize composing small scale, orthogonal data types to build programs, which intrusive containers basically turns inside out.
Intrusive containers almost seem like something a clever functional compiler should do for you, ie. it's a program transformation kind of like array of structs can be functionally transformed to a more efficient struct of arrays. A flow sensitive analysis identifies the collections to which an object might be added, and adds the requisite bookeeping info to the data type during compilation. Would be an interesting research topic at least.
[1] For instance, in a kernel, a process could be waiting on multiple file descriptors, or timers, or any number of other things, all of which might have their own queues to which the process might be added.
#include <stdio.h>
#define P
#define T int
#include <vec.h>
int compare(int* a, int* b) { return *b < *a; }
int main(void)
{
vec(int) a = vec_init(int)();
vec_push_back(int)(&a, 9);
vec_push_back(int)(&a, 1);
vec_push_back(int)(&a, 8);
vec_push_back(int)(&a, 3);
vec_push_back(int)(&a, 4);
vec_sort(int)(&a, compare);
foreach(vec(int), &a, it)
printf("%d\n", *it.ref);
vec_free(int)(&a);
}
I really should finish that writeup some day.You can, however, use a script to generate some of the preprocessor boilerplate while still having the templates configurable and instantiable with an ordinary C compiler. This is how my metaprogramming library Pottery works:
https://github.com/ludocode/pottery
It uses #include for templates rather than code block macros, something the article doesn't really go into. It's more powerful this way and the templates are far more readable; aside from the generated metaprogramming boilerplate, the templated code looks like (and preprocesses to) ordinary C.
GObject is a great example of this. It's a library for doing OOP with C, rather than switching to C++. It's clunky enough that they ended up making a whole new language which compiles to C/GObject, called Vala.
Vala rarely gets attention these days, but it's worth reading about. https://en.wikipedia.org/wiki/Vala_(programming_language)
Yes, basically it is C OO ABI, however only masochists would use it as such.
Everyone else just reaches for VB 6, C++ (MFC, ATL, WRL, WIL, C++/CX, C++/WinRT, yeah I know, so is WinDev....), .NET, Delphi, C++ Builder, ...
All modern Windows APIs use COM to great extent, anyone that uses pure Win32 APIs is basically frozen in Windows XP view of the world.
Mini-history lesson, initially there was DDE, which allowed for basic IPC on Windows 3.x, then came OLE 1.0, both required endless amount of C boiler plate.
Then someone realized that from all concepts, there was a set of central ideas and Windows OO ABI was born, which basically maps to structure with function pointers, naturally with the same layout as Windows C++ compilers organize their vtables when using single inheritance.
So then we had OLE 2.0, COM, OCX, ActiveX, mini-COM, which are basically all the same, IUnknown interface with three methods QueryInterface(), AddRef() and Release(), everything else builds on top of that.
WinRT/UWP is COM updated with some of the ideas that were actually behind .NET birth, so alongside IUnknown there is IInspectable.
So on Windows any language that understands COM ABI (not only C++) can make use of an OS wide OO ABI.
Then there are lots of things one can do with it, regarding in which memory space those components run, how many threads they use, security,...
Going back to the initial question, it is designed to be called from C as well, but almost no one does it.
Of course it has no practical value but it was really cool to see it was possible to do so.
I've also found use in them in combination with `_Generic` for implementing generic containers/data structures. Of course I don't use these all the time by any means but if I'm going to be using a complex data structure I might as well just use an xacro to do a glorified copy-paste for the structs and accessors. It's all type safe, doesn't make the code any less readable IMHO, and it's surprisingly very debugger friendly.
The xacros used for this are all together only about 10 lines of code but they've saved me countless hours of work/headache over the years and I've never once seen them blow up in a way that isn't immediately diagnosable and fixable.
I understand that macros are by no means to be used everywhere but I do find that macros/xacros provide an incredible amount of utility when putting together "library" or "HAL" code where there's a well defined interface but the internals can largely be hidden from the user/developer.
Of course I'd generally just prefer to use C++ but when that's not an option or would add undue friction, I find macros/xacros to be a useful tool for a developer.
But this article is adding tweaks to C control flow that really I could live without and are just about complicated enough to scare and waste the time of anyone stuck maintaining it, that's my concern. It's a real concern, based on my own experience and watching the many C programmers around me tackle fixing or upgrading such code. Macros that try to be "too smart" and try to simplify or make some control structure in C more elegant with hidden complexity and caveats under its preprocessor hood are harder to maintain, I don't think the 'nicer' code you get is worth the extra work maintaining it. It's probably applied with the least potential cost in a code base that will be written and maintained by only one person, but most professional C you need to assume will be maintained by other people as well as yourself. That's the angle I'm coming from.