Table of Contents:
Introduction
In a language like python you have the concept of **kwargs as a function parameter. This special double * syntax allows you to pass in named arguments captured as a dictionary, like the example below:
def print_item(**kwargs):
# kwargs is a dictionary
print(f"Item name: {kwargs['name']}")
if "kind" in kwargs:
print(f"Kind: {kwargs['kind']}")
print(f"Quantity: {kwargs['quantity']}")
print(f"Liters: {kwargs['liters']}")
print_item(name="Milk", quantity=2, liters=1)
# output:
# Item name: Milk
# Quantity: 2
# Liters: 1
This allows for a convenient way to pass arguments by their name in any order you'd like and have optional parameters without complicating the function signature.
What if we could do this in C? (spoiler: there is a way)
Implementation in C
Through the magic of the preprocessor we can accomplish this effect, optional parameters and all.
First lets define our item structure.
struct item_t {
const char *name;
const char *kind;
int quantity;
float liters;
};
Next we'll define our print function like you would normally in C.
// name it with `__t` for the concrete function.
void print_item__t(struct item_t item) {
printf("Item name: %s\n", item.name);
if (item.kind != NULL) {
printf("Kind: %s\n", item.kind);
}
printf("Quantity: %d\n", item.quantity);
printf("Liters: %f\n", item.liters);
}
Now we can write the magically preprocessor part.
// name the macro what we want and use `...` to signify va_args
// Then call our function with a structure with default values
// and the va_args at the end to overwrite any values.
#define print_item(...) \
print_item__t((struct item_t){ \
.name = "default", \
.kind = NULL, \
.quantity = 0, \
.liters = 0, \
__VA_ARGS__ \
})
So lets break down this macro.
- First we define the macro name and specify we are using va_args with
...as the parameter.
#define print_item(...) \
- Next we call the function with a structure of our
item_twith default values.
print_item__t((struct item_t){ \
.name = "default", \
.kind = NULL, \
.quantity = 0, \
.liters = 0, \
- Last we pass in the
__VA_ARGS__values at the end of our default structure. This works because C allows the later defined properties to overwrite earlier ones.
.liters = 0, \
__VA_ARGS__ \
})
Here's how all of this would be structured in actual files.
-
item.h
#ifndef ITEM_H
#define ITEM_H
struct item_t {
const char *name;
const char *kind;
int quantity;
float liters;
};
void print_item__t(struct item_t item);
#define print_item(...) \
print_item__t((struct item_t){ \
.name = "default", \
.kind = NULL, \
.quantity = 0, \
.liters = 0, \
__VA_ARGS__ \
})
#endif
-
item.c
#include "item.h"
#include <stdio.h>
void print_item__t(struct item_t item) {
printf("Item name: %s\n", item.name);
if (item.kind != NULL) {
printf("Kind: %s\n", item.kind);
}
printf("Quantity: %d\n", item.quantity);
printf("Liters: %f\n", item.liters);
}
Using our Implementation
Now we can use our macro similar to the python example in the introduction.
#include "item.h"
int main(void) {
print_item(.name = "Milk", .quantity = 2, .liters = 1);
return 0;
}
// output:
// Item name: Milk
// Quantity: 2
// Liters: 1
Conclusion
Through preprocessor magic we can achieve a syntax convenience with calling functions that have struct parameters as options. This can be extremely useful for functions with lots of optional parameters and\or default values. However, preprocessor magic is something that is either loved or hated so you may want to consider if it's worth implementing in your project or not.
Top comments (8)
You made a small mistake with the comments at the end (instead of
//or/*you used#)I really liked the idea of making parameters optional, even though I know you mentioned creating a Python-like way to interact with parameters
This method allows for optional parameters, and if you think about it further, you can implement mandatory parameters as well
Speaking of C syntax tricks, I once played around with using the dot "similar to how namespaces are used."
My favorite example is a base64 library:
And now we can use
b64.encodeandb64.decodethat looks coolI figured since we were doing silly preprocesses magic to achieve a Python-like syntax, it would be understood it’s not going to be “the most efficient” way.
As far point 2, this is a good call out. clang warns by default and gcc warns about it with
-WextraBut you can do it efficiently. The technique you describe has been around for decades and I'm sure is used in real-world projects, so might as well show an efficient implementation.
As far as the warning, more specifically, it's controlled by the
-Woverride-initwarning. You can suppress the warning by use of_Pragma:Or something like that.
What would an efficient implementation look like?
Generating a temp variable in the macro?
I am curious what you would do differently
I already said in my original point 1: pass by pointer:
C allows you to put
&in front of a structure literal; then just use it in the function via pointer.You always have to construct a temporary. In your original code, a temporary is constructed then copied into the function's parameter since it's by value. In my version, the same original temporary is created, but no copy is made.
Oh gotcha, it does feel weird to take a pointer of a non-bound temporary. (I can’t really explain why it feels weird other than personal preference)
But that should be on the implementer if they hold on to the pointer longer than the function.
I guess I thought you were referencing something more than that approach. But that makes sense! I misunderstood your original comment
FYI, the lifetime of such a temporary is the scope in which it's defined, so:
The lifetime of the temporary persists until the
}of theif, so inside for the firstputs, it's fine; but for the secondputs, it's undefined.Granted, you should likely never set such a global pointer to a temporary regardless; but it's just to illustrate the rule.