It’s inevitable that some functions’ signatures become unwieldy. This is especially true as a project ages, and various developers who were not involved in the initial vision are asked to extend and tweak various existing functionality.
In a high-level language, it’s often possible to effect new changes in an existing function without modifying all its consumers.
In C however, that’s not the case (unless the function already happens to use va_args). Once a C function’s signature changes, all consumers must be modified. Things get even more complicated if the function is part of a public API in a library.
It occurred to me that there are existing features of C that allow us to pre-emptively design functions in a way that future-proofs them in anticipation of these types of changes. I’m not advocating that all functions be designed this way, however for the the class of functions that appear candidates for future signature expansion, my proposal may provide some ideas.
Existing stepping stones
The main features we’ll be putting together for this method are:
In C all function parameters and returns are passed by-value, not by reference. That means the callee receives a copy of the parameters, and can not influence the caller’s copy.
Perhaps for the sake of performance, C developers generally assume the above rule leads to “structs must be passed as a pointer” to allow that copying to be performed, however that is not true. It’s perfectly valid in C to call a function passing it a struct (not a pointer to a struct). The function’s signature would look like this:
The caller would call this function like so:
say_hello would receive a COPY of that struct, which means its local p will be at a different address than bob. The inner members of p will be copied, individually, to the values of the inner members of bob
Initializing a variable such as an int can be done like so:
Similarly, initializing a struct can be done like so:
The individual struct members are given, between brackets, in the same order as the struct declaration.
Missing members are allowed:
In the above case, any members not supplied will be initialized to 0 / NULL.
It’s perfectly legal to not supply any members, in which case all members will be set to 0 / NULL:
say_hello which accepts a struct may be called using an inline-initialized struct like so:
Some compilers may need the struct explicitly cast, like so:
C99 Designated initializers
When initializing a struct, you have the option of referencing its individual members by name, instead of by position:
Again, as with position-based initialization, members may be omitted to default them to 0 / NULL:
While macros need no introduction to any seasoned C programmers, they’re typically declared to accept a fixed set of arguments. Variadic macros allow for a dynamic set of arguments to the macro. We’ll be using them for some final syntactic sugar, but they’re not strictly necessary.
Putting it all together
We’ll assume we’re writing a find function that, given a set of criteria, searches an employee directory and returns matches.
The implementation of the search and returning the result aren’t important. What is however is designing a signature for that function that anticipates inevitable future changes to the supplied search criteria.
The implementation of find itself, for the purpose of demonstrating consuming the input, looks like so:
Finally, the consumer consumes it using the Find macro (notice the capitalization). Here’s a battery of consumption cases:
When run, the output is:
End result and viability
The end result is demonstrated in the main function above.
The pros are:
- Extremely clear naming for the arguments being given to the function
- No need to supply arguments that are not needed
- Ability to add new members to the struct without altering existing code that consumes that function
The cons are:
- There’s probably more overhead to copying structs in general compared to copying the same number of elements declared as regular function parameters
- Missing members defaulting to 0 / NULL may be logically ambiguous if 0 / NULL is a logically acceptable explicit value for a given member. That is why the above example explicitly sets min_age / max_age to -1 to indicate non-interest. Another approach would be to use a secondary member to indicate the desire to “use” the first member. For example, age coupled with use_age
- Variadic macros may not be available everywhere. This method still works without them, at the cost of more verbosity for the function consumer.
So far, the above has just been ideas and experimentation on my part. It’s not something I’ve used in any actual projects I’m working on.
If you’re a C developer I’d love to hear your thoughts on this approach.