Optional arguments: simple case#
Work In Progress
This page is a draft
Let’s see how the tricks introduced in the previous chapter can be used to simulate optional arguments with a default value.
Definition
Optional arguments is a language feature that allows the caller of a function not to provide certain arguments, in which case a default value is used, chosen by the callee.
Here is an example in C++:
uint64_t fibonacci(uint8_t n, uint64_t f0 = 0, uint64_t f1 = 1)
{
switch (n)
{
case 0: return f0;
case 1: return f1;
default: return fibonacci(n - 1, f1, f0 + f1);
}
}
This allows having the expected interface with a single argument, while also allowing the caller to choose the first two terms of the sequence.
The caller may then write:
// Starting with 0 1
fibonacci(7); // 13
fibonacci(7, 0); // 13
fibonacci(7, 0, 1); // 13
// Starting with 1 1
fibonacci(6, 1); // 13
fibonacci(6, 1, 1); // 13
// Historical sequence
fibonacci(5, 1, 2); // 13
// Custom sequence
fibonacci(5, 5, 10); // 40
We are going to demonstrate how to achieve a similar interface in C.
Single optional argument#
Let’s start with a simpler example: the factorial of an integer.
Why is there a second argument to factorial ?
The most straightforward implementation looks like:
unsigned long factorial(unsigned char n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
And in fact, it is all you need, because your compiler will most likely optimize it to a loop with constant memory usage. But to please your computer science professor we’ll write an implementation with explicit tail call optimization.
static // Tell the compiler this function will not be called outside this compilation unit, so it won't create a symbol for it and just inline it
unsigned long factorial_aux(unsigned char n, unsigned long accumulator)
{
if (n <= 1)
return accumulator;
__attribute__((musttail)) // We can even ask the compiler to issue an error if tail recursion isn't possible
return factorial_aux(n - 1, n * accumulator);
}
unsigned long factorial(unsigned char n)
{
return factorial_aux(n, 1);
}
What you can see is that the factorial
function is just a wrapper to provide a default value. Which is perfectly fine as the auxillary function will be inlined inside, but let’s act like it’s an unforgivable faux pas. Using obscure preprocessor tricks to solve non-existant problems is what we are here for anyway.
Also, if you really care about performance, just use a lookup-table: there’s only 20 factorial values that can fit on 64 bits
A first attempt is to replace the wrapper function with a wrapper macro that has the same name:
unsigned long factorial(unsigned char n, unsigned long);
#define factorial(N) factorial(N, 1)
unsigned long factorial(unsigned char n, unsigned long accumulator)
{
if (n <= 1)
return accumulator;
__attribute__((musttail))
return factorial(n - 1, n * accumulator);
}
#include <factorial.h>
int main()
{
unsigned long x = factorial(10);
}
This works because:
in the header, the function declaration is not affected by the macro as the latter is defined after.
macros cannot be recursive, so once
factorial(10)
is replaced byfactorial(10, 1)
, no further expansion is performedfactorial.c
does not includefactorial.h
, so it is not affected by the macro
1#include <stdio.h> // printf
2
3#define eval(x) printf("%s = %lu\n", #x, x)
4
5unsigned long _factorial(unsigned short n, unsigned long accumulator)
6{
7 if (n == 1)
8 return accumulator;
9 __attribute__((musttail)) return _factorial(n - 1, accumulator * n);
10}
11
12#define FACTORIAL_(N, ACC, ...) _factorial(N, ACC)
13#define FACTORIAL(N, ...) FACTORIAL_(N, ##__VA_ARGS__, 1)
14
15int main()
16{
17 eval(FACTORIAL(5));
18 eval(FACTORIAL(10));
19 eval(FACTORIAL(12));
20}
1#include <stdio.h> // printf
2
3#define eval(x) printf("%s = %lu\n", #x, x)
4
5unsigned long _factorial(unsigned short n, unsigned long accumulator)
6{
7 if (n == 1)
8 return accumulator;
9 __attribute__((musttail)) return _factorial(n - 1, accumulator * n);
10}
11
12#define FACTORIAL_(N, ACC, ...) _factorial(N, ACC)
13#define FACTORIAL(N, ...) FACTORIAL_(N, ##__VA_ARGS__, 1)
14
15int main()
16{
17 eval(FACTORIAL(5));
18 eval(FACTORIAL(10));
19 eval(FACTORIAL(12));
20}
unsigned long _factorial(unsigned short n, unsigned long accumulator)
{
if (n == 1)
return accumulator;
__attribute__((musttail)) return _factorial(n - 1, accumulator * n);
}
int main()
{
printf("%s = %lu\n", "FACTORIAL(5)", _factorial(5, 1));
printf("%s = %lu\n", "FACTORIAL(10)", _factorial(10, 1));
printf("%s = %lu\n", "FACTORIAL(12)", _factorial(12, 1));
}
FACTORIAL(5) = 120
FACTORIAL(10) = 3628800
FACTORIAL(12) = 479001600
1#include <sys/types.h> // uint*_t
2
3#include <inttypes.h> // PRI*
4#include <stdio.h> // printf
5
6#define FIBONACCI(N, A, B, ...) fibonacci(N, A, B)
7#define fibonacci(N, ...) FIBONACCI(N, ##__VA_ARGS__, 1, 1)
8
9uint64_t fibonacci(uint16_t n, uint64_t f0, uint64_t f1)
10{
11 switch (n)
12 {
13 case 0: return f0;
14 case 1: return f1;
15 default: return fibonacci(n - 1, f1, f0 + f1);
16 }
17}
18
19int main()
20{
21 printf("%" PRIu64 "\n", fibonacci(6));
22 printf("%" PRIu64 "\n", fibonacci(7, 0, 1));
23 printf("%" PRIu64 "\n", fibonacci(5, 1, 2));
24}
1#include <sys/types.h> // uint*_t
2
3#include <inttypes.h> // PRI*
4#include <stdio.h> // printf
5
6#define FIBONACCI(N, A, B, ...) fibonacci(N, A, B)
7#define fibonacci(N, ...) FIBONACCI(N, ##__VA_ARGS__, 1, 1)
8
9uint64_t fibonacci(uint16_t n, uint64_t f0, uint64_t f1)
10{
11 switch (n)
12 {
13 case 0: return f0;
14 case 1: return f1;
15 default: return fibonacci(n - 1, f1, f0 + f1);
16 }
17}
18
19int main()
20{
21 printf("%" PRIu64 "\n", fibonacci(6));
22 printf("%" PRIu64 "\n", fibonacci(7, 0, 1));
23 printf("%" PRIu64 "\n", fibonacci(5, 1, 2));
24}
uint64_t fibonacci(uint16_t n, uint64_t f0, uint64_t f1)
{
switch (n)
{
case 0: return f0;
case 1: return f1;
default: return fibonacci(n - 1, f1, f0 + f1);
}
}
int main()
{
printf("%"
"l"
"u"
"\n",
fibonacci(6, 1, 1));
printf("%"
"l"
"u"
"\n",
fibonacci(7, 0, 1));
printf("%"
"l"
"u"
"\n",
fibonacci(5, 1, 2));
}
13
13
13