Tuesday 25 April 2017

Too many arguments; or: keyword arguments in C

Sometimes you extend a generic helper function and it starts to take too many optional arguments, which can be left as NULL if they don't matter.

Maybe your function formats and signs a message that could have quite a variety of fields.

int sign(struct key *key, const char *this, const char *that)
{
    x_sign(key, this);
    ...
}
...

int result = sign(key, "This", "That");

and then you find yourself wanting to add const char* other.

Yeah... and there will be a lot of caller code to fixup (unless you want to use the other macro trick for default arguments, which I'll write up later). 

Or will there?

struct sign_args {
    const char* this;
    const char* that;
    const char* other;
};

int sign(struct key *key, struct sign_args *args)
#define sign(key, ...) sign(key, &(struct sign_args){ __VA_ARGS__ })
{
    x_sign(key,args->this);
    ...
}
and it is called the same way:

sign(key, "This", "That", "Other");

but you also get keyword arguments for free!

sign(key, .other = "Other"); // leaves this & that NULL

And this expands out to a valid compound literal expression. The temporary struct (an lvalue) is created (probably on the stack) and disposed of automatically at the end of the expression.

sign(key, &(struct sign_args){ .other = "Other" });

Another advantage is that now as your arguments are passed as a pointer, it is very easy to tunnel them all through the usual void* callback systems. In the case below, it briefly obtains an interactively unlocked key to sign with:

int batch_sign(struct key *key, struct sign_args *args);
int sign(struct key *key, struct sign_args *args) 
#define sign(key, ...) sign(key, &(struct sign_args){ __VA_ARGS__})
{
  x_with_unlocked_key(key, batch_sign, args);
  ...
}

Is it efficient? Every time the args are passed, only a pointer is passed, so that can be efficient; but the struct does have to be created once and may be a little more work than passing a struct as an argument or passing the arguments.

The main points to note are that

  • the original arguments should keep their same order at the start of the args struct
  • as the macro has the same name as your function, be sure to define it AFTER your function
  • the arguments are passed by pointer and so shared as they are tunnelled.
    This can make it easy to pass back a temporary value from an auto-variable. 
Of course you don't have to pass a pointer to the struct, you could pass the struct, by removing the & from the macro:

int sign(struct key *key, struct sign_args *args)
#define sign(key, ...) sign(key, (struct sign_args){ __VA_ARGS__ })
{
    x_sign(key,args.this);
    ...
}
it is called the same way:

sign(key, "This", "That", "Other");

but expands out slightly differently, passing the struct instead of a pointer to it, e.g.:

sign(key, .other = "Other"); // leaves this & that NULL

which this expands out to:

sign(key, (struct sign_args){ .other = "Other" });

Of course there is still nothing to prevent you from passing &args via a callback, or making and passing a copy.