This post is for programmers who like C or for one reason or another can't use anything else but C in one of their projects. The advantages of having default arguments is not something that needs convincing. It's just very nice and convenient to have them. C++ offers the ability to define them but C under the C99 standard has no way to allow it. In this post I will detail two ways I know of implementing default arguments in C. If a reader happens to know additional ways please share in the comments
Suppose we have a struct that contains some data and we want to initialize it
//! The struct we want to initialize typedef struct foo { //! The first 3 parameters get initialized during the init function int an_int; char a_char; float a_float; //! The other are just parameters used later char type; char runTimeFlag; }foo;
Method 1
The first method is my favorite and works in C99 compliant C-code. The code is by no means originally my invention and all credit goes to Jens Gustedt. He details this method and other amazing preprocessor trick in his blog. He also has created a collection of headers that one can include in his project that provides all of the tricks he writes about. It is called P99 and can be found here.
Using this method we have to define 3 different functions for all the different ways of initializing the default arguments.
//! Let's define the default value for the 3rd argument #define DEFAULT_3 3.14 //! Let's define the default value for the 2nd argument #define DEFAULT_2 42 //! Let's define the default value for the 1st argument #define DEFAULT_1 1337 //! A function to initialize a foo with all 3 arguments foo* def_foo_init3(int arg1,char arg2,float arg3) { foo* ret = malloc(sizeof(foo)); ret->an_int = arg1; ret->a_char = arg2; ret->a_float = arg3; return ret; } //! A function to initialize a foo with only the first 2 arguments foo* def_foo_init2(int arg1,char arg2) { //call the full initializer but with the default value for the 3rd argument return def_foo_init3(arg1,arg2,DEFAULT_3); } //! A function to initialize a foo with only the first argument foo* def_foo_init1(int arg1) { //call the full initializer but with the default value for the 2nd and 3rd argument return def_foo_init3(arg1,DEFAULT_2,DEFAULT_3); } //! A function to initialize a foo with no arguments foo* def_foo_init0() { //call the full initializer with default values return def_foo_init3(DEFAULT_1,DEFAULT_2,DEFAULT_3); }
Now you might be thinking, and so what? What if we have different functions that use the default arguments in C if we have to call the appropriate function ourselves? You would be right to ponder that, and here is where the preprocessor magic comes in. Define all these macros in a header. Alternatively you can use the P99 library that has them defined for you already.
//! A macro used to count the number of arguments. Can go up to 64 which according to the original author is the limit for C #define RF_ARG16(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, ...) _15 //! Just a macro that has a reverse sequence of the 16 numbers above #define RF_ARG16_RSEQ() 15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 //! This macro is one of those which do the magic. It returns the number of arguments that have been passed as parameters to the macro //! @warning This does not work on an empty list of arguments. To circumvent this problem we will see a trick further down #define GET_NARG_NOEMPTY(...) RF__NARG_NOEMPTY_IMP(__VA_ARGS__,RF_ARG16_RSEQ()) #define RF_NARG_NOEMPTY_IMP(...) RF_ARG16(__VA_ARGS__)
So now if you use the GET_NARG_NOEMPTY() on a list of arguments it will return the number of arguments given. But how does it work? Well imagine this test case. Say we pass a list of 3 arguments (54,23,2.345) inside the macro like below
GET_NARG_NOEMPTY(54,23,2.345) // Will unfold to==> RF_NARG_NOEMPTY_IMP(54,23,2.345,RF_ARG16_RSEQ()) // Will unfold to==> RF_NARG_NOEMPTY_IMP(54,23,2.345, 15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0) // Will unfold to==> RF_ARG16(54,23,2.345, 15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0) // And from the definition of RF_ARG16 we get the 16th number in the list and this will unfold to ==> 3
Pretty amazing right? I thought so too when I first learned that trick. But as we mentioned before this has some limitations. Specifically it will not work for an empty list as it will still return 1. Quoting Jens Gustedt directly "So in fact GET_NARG_NOEMPTY is cheating. It doesn’t count the number of arguments that it receives, but returns the number of commas plus one. In particular, even if it receives an empty argument list it will return 1"
To rectify that we will define some additional macros below. This is the nicest part of the trick in my opinion and this is how I came to learn about Jens Gustedt's blog by trying to find a way to make this macro work for any number of arguments, including the no arguments case
//! A macro that pastes 2 tokens together using the ## operator #define RF_PASTE2(__0,__1) __0 ## __1 //! A macro that pastes 5 tokens together using the ## operator #define RF_PASTE5(_0, _1, _2, _3, _4) _0 ## _1 ## _2 ## _3 ## _4 //! This is a macro using the same functionality as above to see if the argument list has a comma. Returns 1 if it does and 0 if not #define HAS_COMMA(...) RF_ARG16(__VA_ARGS__, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0) //! This one is essential to the succesfull working of this trick. Check below for the usage explanation #define RF_TRIGGER_PARENTHESIS_(...) , //! This is the soul of the macros checking if the arguments list is empty //! It tests for 4 different cases detailed in the comments below. Each one returns 0 for false and 1 for true #define IS_EMPTY_NARG(...) \ _ISEMPTY( \ /* test if there is just one argument, eventually an empty \ one */ \ HAS_COMMA(__VA_ARGS__), \ /* test if RF_TRIGGER_PARENTHESIS_ together with the argument \ adds a comma */ \ HAS_COMMA(RF_TRIGGER_PARENTHESIS_ __VA_ARGS__), \ /* test if the argument together with a parenthesis \ adds a comma */ \ HAS_COMMA(RF_VA_ARGS__ (/*empty*/)), \ /* test if placing it between _TRIGGER_PARENTHESIS_ and the \ parenthesis adds a comma */ \ HAS_COMMA(RF_TRIGGER_PARENTHESIS_ __VA_ARGS__ (/*empty*/)) \ ) //! This macro tests the results of the above macro. The 4 different arguments are the results of the 4 different checks #define RF_ISEMPTY(_0, _1, _2, _3) HAS_COMMA(RF_PASTE5(RF_IS_EMPTY_CASE_, _0, _1, _2, _3)) //! This is the definition of the only case of the above checks we care about #define RF_IS_EMPTY_CASE_0001 ,
So what's happening here? With RF_TRIGGER_PARENTHESIS_() macro we are taking advantage of the fact that a function macro that does not get followed by its parentheses will be left alone. Notice how it is used. By putting the arguments between the macro and its parentheses as done in the 4th check above we basically make sure that the macro will work only if there are no arguments
RF_TRIGGER_PARENTHESIS_ __VA_ARGS__ (/*empty*/) //the RF_TRIGGER_PARENTHESIS__ macro will only fire up and unfold to a comma if __VA_ARGS__ is empty
The IS_EMPTY_NARG(...) macro tests for 4 different special cases that we first of all have to take care of. These are
- The argument list has a comma already (Retuns 1 if yes and 0 if not)
- RF_TRIGGER_PARENTHESIS_ together with the argument add a comma (Retuns 1 if yes and 0 if not)
- The argument together with a parentheses add a comma (Retuns 1 if yes and 0 if not)
- The argument is empty and so being placed between RF_TRIGGER_PARENTHESIS_ and a parentheses adds a comma (Retuns 1 if yes and 0 if not)
So the IS_EMPTY_NARG(...) macro tests for the above cases and returns _IS_EMPTY_CASE_XXXX depending on the result of the checks by pasting the tokens. The only results that we actually care about it RF_IS_EMPTY_CASE_0001 which basically means that the argument list is empty and that is why it is defined above.
Now we are almost there. Since we have a way to detect if the argument list is empty or not and also a way to get the number of arguments it's only a matter of having a macro to implement both checks and return us the results. So finally define the macros given below
//! Two different helper macro that evaluate if the variadic list is empty or not. #define RF_NARG_EMPTY_1(__V__) 0 #define RF_NARG_EMPTY_0(__V__) __V__ //! THIS is the macro that actually counts the number of arguments that are passed to it and returns it. Works for empty list too. //! >>THIS DOES ALL THE WORK<< #define GET_NARG(...) RF_NARG__1(IS_EMPTY_NARG(__VA_ARGS__), GET_NARG_NOEMPTY(__VA_ARGS__)) //! Helper macro. Takes in the result of if the variadic list is empty (1) or not (0) and pastes it to RF_NARG_EMPTY_ #define RF_NARG__1(B, __V__) __NARG__2(RF_PASTE2(RF_NARG_EMPTY_, B), __V__) //! Helper macro. Basically decides what RF_NARG returns, which is either __V__ the value of GET_NARG_NOEMPTY or 0 in the case of NARG_EMPTY() evaluating to 0 #define RF_NARG__2(B, __V__) B(__V__)
So let's see a working example now. Suppose we have an empty argument list (). Let's pass it to our macro and see how and why it works
GET_NARG(/*empty*/) //==>unfolds to RF_NARG__1(IS_EMPTY_NARG(/*empty*/), GET_NARG_NOEMPTY(/*empty*/)) //==>unfolds to //as stated from the previous example the GET_NARG_NOEMPTY case does not work for empty arguments so returns 1 here which is wrong //but the IS_EMPTY_NARG also returns 1 which shows that the arg list is empty RF_NARG__1(1, 1) //==>unfolds to RF_NARG__2(RF_PASTE2(RF_NARG_EMPTY_,1), 1) //==>unfolds to RF_NARG__2(RF_NARG_EMPTY_1, 1) //==>unfolds to RF_NARG_EMPTY_1(1) //==>unfolds to 0
So we get the result that the argument list is empty! Pretty neat huh? Also in the case that IS_EMPTY_NARG() returned 0 which meant that the argument list was not empty then the results would have finally unfolded to RF_NARG_EMPTY_0(ACTUAL_NUM_OF_ARGS) which would return the actual number of arguments.
So finally let's see how to apply this to our foo struct example. We have already defined the 3 different functions and each one uses default arguments where needed so it's only a matter of calling the correct function where needed. So to close this method define the macros below. Note that these macros can be used for anything not just the foo example
//! This macro selects the function name according to the arguments given and passes both the name and the arguments to the next macro #define RF_SELECT_FUNC(FUNCNAME,ARGSN,...) RF_RUN_FUNC( RF_PASTE2(FUNCNAME,ARGSN),__VA_ARGS__) //! This macro runs the function take from select function with the given argument #define RF_RUN_FUNC(FUNCNAME,...) FUNCNAME(__VA_ARGS__) //! This is the only example specific macro which calls the appropriate _foo_init function depending on the number of arguments #define foo_init(...) RF_SELECT_FUNC(def_foo_init,GET_NARG(__VA_ARGS__),__VA_ARGS__)
As you can see from the code above it's just a matter of getting the correct number of arguments by the "magic macro" GET_NARG() defined above and then pasting that to the function name to choose the correct function and then pass the arguments to that function. This ofcourse has the prerequisite that you have defined your functions like I did in my example but it's quite easy to alter this when/if needed.
Method 2
This post is getting a lot bigger than I initially expected so I am sorry but bear with me. The second method is quite different from the first and does not use the preprocessor that much. Instead we have to define an argument list with the parameters like below:
//! This is the argument list for initializing a foo object typedef struct foo_args { int arg1; char arg2; float arg3; }foo_args;
After that we define a function that initializes foo with the foo argument list and a macro that takes in variadic arguments and calls said function.
//! This is the function that initializes a foo object by taking in a foo argument list foo* def_foo_arg_init(foo_args args) { int arg1 = (args.arg1 != 0) ? args.arg1 : DEFAULT_1; char arg2 = (args.arg2 != 0) ? args.arg2 : DEFAULT_2; float arg3 = (args.arg3 != 0) ? args.arg3 : DEFAULT_3; return def_foo_init3(arg1,arg2,arg3); } //! This is the macro that does all the work and uses a quite smart trick, explanation follows below #define foo_init(...) def_foo_arg_init((foo_args){__VA_ARGS__})
The foo_init() macro takes a variable number of arguments just like Method 1 but in this case here we put all these arguments into a braces enclosed list {...} and and the compound literals are interpreted as an object of foo_args. What this basically achieves is to populate the foo_args object with the parameters values and if there are not enough parameters pad it with zeroes. So when the def_foo_arg_init function is called with the foo_args object it checks each of the arguments of the list if it has a value (is not zero) and if it does, keeps that value or if it's zero it then uses the default value.
One obvious and huge disadvantage of this method is that you can't use it to give value of 0 to parameters unless it's also the default value. This is the main reason this method is not my preferred one. On the other hand many people argue that 0 should not be a value and should always state the absence of a value in a program.
The advantage of this method is that you can basically do A LOT of processing of the arguments by doing some work inside _foo_arg_init and by having a smart foo_args object. You can actually process cases where the arguments are not given in the series you expect by having paddings inside the foo_args object e.t.c. The limit is ,as always, the programmer's imagination.
Conclusions
This post became a lot bigger than I expected. I presented two methods I know for implementing default arguments in the C language which are compliant with the C99 standard. Credit for the first method goes to Jens Gustedt and for the second method I am afraid I don't know the original author. I personally prefer the first method even though in cases where I want to accept arguments in any order and do lots of processing on them I use the second one. Another option is to combine both of them by using the GET_NARG() macro to also know the number of arguments passed to the function when using the second method. If you know of any other methods please share with me in the comments. I would be ecstatic to learn new ways to accomplish them. Also if you find any mistakes let me know so that I can correct them. Thanks for reading!