A friend and I were talking about architecture choices on embedded systems software when a question arose: Is it really a good choice to apply template meta-programming in order to avoid opaque pointers?
Well, the answer is complicated, there’s never one best solution for everything, but before digging into comparisons, here's a bit of fundamentals and an implementation example using both solutions. If you are already familiar with the concepts and construction of each pattern you can jump to the next header of the article.
What is template meta-programming? Well, to make it short, you use a template when you instruct the compiler to generate code for you. It is a built-in feature of C++ language where you have the `template` keyword to implement it, but not in C. This doesn’t means we cannot write C preprocessor directives that will generate code for us: Actually, that’s exactly what I propose we do whenever appropriate.
What about opaque pointers? This one is easy and you probably know it. In C, you are using an opaque pointer whenever you cast a variable’s address to `void *`:
void * opaque_pointer = (void*)&i;
As an example applying both design patterns, consider the following specification: to build a function that stores and other that loads any given data, type independently.
Implementing with opaque pointer should be easy: first we create a pair of functions to store and load the data that takes a void pointer and size as argument.
void store_opaque (const void * data, size_t data_size)
memcpy(some_storage_space, data, data_size);
void load_opaque (void * data, size_t data_size)
memcpy(data, some_storage_space, data_size);
And use it this way:
Realize that each function's signature uses opaque pointer to transfer unknown type data, and as a consequence the functions need to know the size of the handled data.
Implementing the example using templates is considerably more complex, we will use macros to rename the function and configure types to it, precisely, the macro named `template_type` is responsible for inserting the user-selected type into the template code.
First we need a “magic” macro to force the expansion of recursive macros, and another that will create function names based on `template_type`:
/* MACRO_CAT expands extra argument and concatenate with the first */
#define MACRO_CAT(a, …) a ## __VA_ARGS__
/* FUNCTION creates the function name ‘f’ with suffix ‘n’ */
#define FUNCTION(f, n) MACRO_CAT(f, n)
Then we are ready to define our template code:
/* Reserved storage Space */
/* The store function */
void FUNCTION(store_, template_type) (const template_type * data)
storage = *data;
/* and the load function */
void FUNCTION(load_, template_type) (template_type * data)
*data = storage;
All template code shown should go in a separate file, let’s call it `store.template`. We separated template code so it can be used as many times as necessary. Now, at the main file we will configure and instantiate the template, using the `#include` directive.
/* Choose the template type */
#define template_type int
/* Including the template will enable the preprocessor to process and generate the defined functions in it to the type configured above */
And we use the template functions this way:
The function names will be populated with the type. This is optional; each template construction may have its own set of requirements. If you want to see a full template example, this link shows a piece of code of an embedded open source project that uses template to create a signal/slot engine.
Comparing Templates and Opaque Pointers
Now that we know how to create templates and make use of opaque pointers, we can compare both techniques.
Writing template code in C has its costs. It is harder to maintain, it takes more lines of code and makes use of some macro trickery, where functions using opaque pointers wouldn’t need to.
Template-generated code might produce more compiled bytes, since each instance will replicate the template code to different signatures. This can be controlled with `static inline` directives if the application allows it to.
On the other hand, sometimes, using template might get more optimized results. This is the case of the example in this article, since the opaque pointer functions had to call stdlib’s memory copy function or to provide their own, while in the template solution it just used the basic assignment operator.
You can do things with the type you wouldn't be able to do if casted to void. For example, if the type is a `struct` you’d be able to access its field inside the template functions. This can be useful to write generics in C, where a template function can use any type that contains a specific field in it.
And last, but not least, it’s hard to mess up the run time when using templates. That’s the main reason to use it. When you cast a type, the statically typed advantages of the C language are lost. Because template doesn’t use, and must not use, any form of casting on its parameters, you will get build errors if any provided type doesn’t meet every single compiler criteria. As an example, take the case of the generics mentioned on the last paragraph: If a provided type doesn’t have the specific field used in the template body, it won’t build. Also, the code simply not build if you build the templates incorrectly; if you configure the template with invalid type; if you use functions with wrong generated names; and so on.
Because of that, templates are a safer choice than opaque data.
That’s the tradeoff. We invest in template development with some extra lines of code and a complicated macro construction to enjoy the safety provided by the compiler static checks. That’s the default choice for languages such as C# and C++, where templates or generics are built-in and does not violate the language's type check. Now, with the suggested construction, we can have it in C too.
So, should we always avoid opaque pointers? No, not at all. The investment of templates in C must be justifiable in your application. There’s no rule of thumb for that; each project has its own dynamics. What I can say is that the investment pays off when you are writing a generic reusable module that might be used at a considerable scale. Imagine how many runtime debugging hours you’ll have avoided for yourself or the module clients by applying the template technique.
I’d like to know what you all think about it. Let me know in the comments.
Thanks for reading.