Recently read in the book, let c language to simulate other languages have named function parameters. I think it's interesting so I'll record it.
goal
It is well known that there is no such thing as a named function parameter in the c language, and although formal parameters have their own names, you can't specify the value of a parameter by that name when passing it.
And in languages that support named parameters, such as python, we can make the code achieve this effect:
def k_func(a, b, c):
print(f'{a=}, {b=}, {c=}')
k_func(1, 2, 3) # Output: a=1, b=2, c=3
k_func(c=1, b=3, a=2) # Output: a=2, b=3, c=1
We want something likek_func(c=1, b=3, a=2)
effect, or at least the expression should be close. Granted, the syntax of c doesn't support such expressions, but there are ways to emulate them.
realization
Let's assume we have such a function in c. Now we want to simulate named parameter passing:
int func(const char *text, unsigned int length, int width, int height, double weight)
{
int printed = 0;
printed += printf("text: %s\n", text);
printed += printf("length: %d\n", length);
printed += printf("width X height: %d X %d\n", width, height);
printed += printf("weight: %g\n", weight);
return printed;
}
The key to simulation is how to accomplish the mapping of names to parameters. And since we have four different types for the five parameters of our function, this mapping also has to be heterogeneous.
Without resorting to third-party libraries, the first thing that comes to mind would be enum plus thevoid*
arrays. enum completes the mapping of names to array indices.void*
Heterogeneous data can be saved.
The disadvantages of this scheme are many: if we want to add a parameter between length and width, we will probably need to change all the mapping code; for example, thevoid*
can accept pointers of any data type, so there is little we can do to ensure type safety, imagine what would happen if someone passed a pointer to text that was an int. So this option is not recommended.
Something that can hold heterogeneous data and at the same time give that data a name is actually very common in C. That's a structure. It's the structure-based program that we're going to choose.
First let's define the structure:
typedef struct func_arguments {
const char *text;
unsigned int length;
int width;
int height;
double weight;
} func_arguments;
It doesn't matter what order the fields are in, you can adjust them any way you like. Now we can set the values based on the field names. Now there's a missing link, only the structure is useless, we need to pass the fields of the structure to the function to make it work. So we're going to write a helper function:
int func_wrapper(func_arguments fa)
{
// Add parameter checksums and other logic as needed.
return func(, , , , , ); }
}
We basically have all the tools we need here, however there's still quite a bit of a gap between now and named parameter passing because we need to write the code this way:
func_arguments fa;
= "text";
= 4;
= = 8;
= 10.0;
func_wrapper(fa);
Not only is the form far worse, the code is cumbersome, so we'll also have to simulate named parameter passing with a little help from c99's new syntax for compound literals + specified initializers:
func_wrapper((func_arguments){ .text = "text", .length = 4, .width = 8, .height = 8, .weight = 10.0 });
c99 allows initialization with. Field name
The fields are set to values in the form of a field, and unspecified fields are initialized to zero. c99 also allows conforming types of literals to be converted to arrays/structs. Using the new syntax we can write the above code.
Now the form is really close, but it still seems a bit verbose. This is where you have to rely on macros. c's macros allow for text substitution and simple type distribution, so you can use them to convert expressions that don't look legal into normal c code.
First of all, don't abuse macros, especially like below, this is just to act as a bit of a record and not to teach you production practices.
With macros you can write it like this:
#define k_func(...) func_wrapper((func_arguments){ __VA_ARGS__ })
Another new syntax for c99 is used here, variable-length argument macros, where the three dots mean that the macro can take any number of arguments separated by commas, and the__VA_ARGS__
will replace these parameters as is. So we just need to make sure that the initializer is specified correctly in the macro's arguments:
k_func(.text="text", .length=4);
// The macro completes the replacement equivalent to func_wrapper((func_arguments){ .text = "text", .length = 4 });
Isn't that amazing?
Some people may be concerned that we're copying the entire structure when we pass parameters, does this pose an efficiency problem? Usually this is not a problem, compilers nowadays are generally able to omit most of the unnecessary copying, and if the object is small, copying it is usually not a big overhead, it's hard to define what small means, in my personal experience anything smaller than two cachelines is usually considered "small".
If you are still unsure, you can also simply change the parameter type to a struct pointer:
int func_wrapper(const func_arguments *fa)
{
return func(fa->text, fa->length, fa->width, fa->height, fa->weight);
}
#define k_func(...) func_wrapper(&(func_arguments){ __VA_ARGS__ })
The usage is the same. Note that the macros in the&
, which will allocate an AUTO lifecycle func_arguments variable (usually on the stack) and then take its pointer. Now you can stop worrying about it. I generally don't recommend writing it this way, though, unless you've gone through performance testing and found that parameter copying is really causing performance problems.
flaws
Miracles and magic aren't free, so the code above like magic tricks comes at a price.
The first flaw, which is relatively minor, is that the field name character must be preceded by a dot. If written that way:printf("%d\n", k_func(.text="text", length=4))
, note that we accidentally left out the dot before length. The compiler will pop up with an unspecified error:
: In function ‘main’:
:31:45: error: ‘length’ undeclared (first use in this function)
31 | printf("%d\n", k_func(.text="text", length=4));
| ^~~~~~
:27:52: note: in definition of macro ‘k_func’
27 | #define k_func(...) func_wrapper((func_arguments){ __VA_ARGS__ })
| ^~~~~~~~~~~
:31:45: note: each undeclared identifier is reported only once for each function it appears in
31 | printf("%d\n", k_func(.text="text", length=4));
| ^~~~~~
:27:52: note: in definition of macro ‘k_func’
27 | #define k_func(...) func_wrapper((func_arguments){ __VA_ARGS__ })
| ^~~~~~~~~~~
It'll tell you.length
The name was never defined instead of telling you to leave out a.
. People who don't usually look at things carefully are going to suffer, because it takes a lot of effort to find this spot.
The second flaw is the lack of syntax hints and auto-completion. After all, macros can replace any text that matches the rules into it, and it doesn't matter what happens after the replacement, so you want to have a good understanding of thek_func
It's not easy to prompt and complete the parameters of a field, and after experimenting with it, there is no ide or editor that can automatically complete the field names for me. But it's not a big problem, because the compiler will report the exact error if it's written incorrectly, but it's just a little less efficient to develop.
The third flaw lies in the fact that it is possible to write something like this:printf("%d\n", k_func(.length=10, .text="text", .length=4))
. We've specified the length field twice, which is syntactically allowed, and the length value will be overwritten by the rightmost one. But this is clearly not what we want, and as I said earlier, since there is no auto-completion or syntax hints, it's hard to notice when we accidentally write weight instead of height. What's worse is that this kind of overriding needs to be specified under gcc.-Wextra
It's only then that you see a painless warning. The same situation under python would just receive a syntax error exceptionkeyword argument repeated
。
There are ways to overcome the first two flaws, the last one can't be helped in any way but by higher level warning settings and human inspection.
summarize
Normally we do.func_wrapper
That step is enough, the later macros don't make much sense purely to formally simulate python's named parameters.
In addition to wrapping in structures, it's also common practice to write a wrapper function and swap the order of the arguments or give default values, but this practice can quickly get the number of interfaces out of control, and a large number of similar interfaces can make the code stink, so I prefer to use structures.
Finally, it's useful to learn new syntax, because a lot of it, when utilized properly, can effectively improve scalability and your development efficiency.