Reputation: 714
I would like to have a function that takes in parameters of generic types, which is possible with variadic functions but I am looking for some suggestions/help.
Basically, the function could accept any primitive types (int, double, char* etc) and certain local structs/pointers to it. I did the following where I hardcode the ENUM values for primitive types and structs.
I came up with a following solution but it doesn't look neat to me at least for the primitive types which I'm using inside ENUM. I see vfprintf
could be used inside a variadic function with parameter being const char *format, ...
but how do I handle structs and multiple parameters with this approach?
enum types {LONG, INT, FLOAT, DOUBLE, CHAR, STRUCT};
typedef struct
{
int size;
char data[64];
} Pkt;
void bar(int len, va_list ap)
{
Pkt pkt = va_arg(ap, Pkt);
printf ("[Bar] packet: %d, %s\n", pkt.size, pkt.data);
}
void foo(int len, ...)
{
char *str;
double val;
int intVal;
va_list ap;
Pkt pkt;
va_start(ap, len);
bar(len, ap);
va_end(ap);
va_start(ap, len);
for (int i=0; i<len; i++)
{
int type = va_arg(ap, enum types);
switch(type)
{
case CHAR:
str = va_arg(ap, char*);
printf ("CHAR: %s\n", str);
break;
case DOUBLE:
val = va_arg(ap, double);
printf ("DOUBLE: %f\n", val);
break;
case INT:
intVal = va_arg(ap, int);
printf ("INT: %d\n", intVal);
break;
case STRUCT:
pkt = va_arg(ap, Pkt);
printf ("STRUCT: %d, %s\n", pkt.size, pkt.data);
break;
default:
break;
}
}
va_end(ap);
}
int main()
{
Pkt pkt = {.size = 50, .data = {"Hello"}};
char *str = "World";
foo(2, STRUCT, pkt, CHAR, str);
return 0;
}
Upvotes: 1
Views: 337
Reputation: 140880
Your approach using enum
is readable and fine - stick with it. If you do not need to pass additional information, like printf
needs with width, flags, precision and modifiers additionally to format specification, then your approach is completely fine.
One suggestion would be to use a sentinel value so that you do not have to count the arguments. Like so:
foo(STRUCT, pkt, CHAR, str, END);
You can (potentially) write a version without the need of any enum
s and format strings. By making the function a macro and overloading the macro on the number of arguments and then for each argument with C11 _Generic
construct adding a "tag" you can distinguish between different types and execute different action. Your code uses enum
to tag all types. The below example uses function pointers:
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
struct pkt {
int size;
char data[64];
};
// note - by passing a pointer to va_list, we are preserving
// the state of va_list and can use it in functions
int _foo_string(va_list *va) {
printf("CHAR: %s\n", va_arg(*va, const char*));
return 0;
}
int _foo_double(va_list *va) {
printf("DOUBLE: %f\n", va_arg(*va, double));
return 0;
}
int _foo_int(va_list *va) {
printf("INT: %d\n", va_arg(*va, int));
return 0;
}
int _foo_pkt(va_list *va) {
const struct pkt pkt = va_arg(*va, struct pkt);
printf("STRUCT: %d, %s\n", pkt.size, pkt.data);
return 0;
}
// is passed a list of:
// <function_pointer, argument,>...., MARK_END
// call function pointer on each argument
int _foo(int unused, ...)
{
va_list va;
va_start(va, unused);
while (1) {
int (*fp)(va_list*) = va_arg(va, int(*)(va_list*));
if (!fp) {
break;
}
if (fp(&va) != 0) {
return -1;
}
}
va_end(va);
return 0;
}
// user of your library can add additional functions here if he wants to
#define _foo_FUNC_USER_ADDITIONAL_TYPES()
// overload the argument on _Generic to find out the function
#define _foo_FUNC(x) _Generic((x), \
_foo_FUNC_USER_ADDITIONAL_TYPES() \
char *: _foo_string, \
const char *: _foo_string, \
int: _foo_int, \
struct pkt: _foo_pkt)
// for each argument, replace argument by "_Geeneric(arg), arg,"
#define _foo_ARGS_1(_1) _foo_FUNC(_1), _1,
#define _foo_ARGS_2(_1, _2) _foo_ARGS_1(_1) _foo_ARGS_1(_2)
#define _foo_ARGS_3(_1, _2, _3) _foo_ARGS_2(_2) _foo_ARGS_1(_1)
#define _foo_ARGS_N(_3,_2,_1,N,...) _foo_ARGS_##N
#define _foo_ARGS(...) _foo_ARGS_N(__VA_ARGS__, 3, 2, 1)(__VA_ARGS__)
// add a trailing MARK_END to detect ending
#define foo(...) _foo(0, _foo_ARGS(__VA_ARGS__) NULL)
int main()
{
struct pkt pkt = {
.size = 50,
.data = "Hello",
};
const char *str = "World";
foo(pkt, str); // just works
return 0;
}
Whatever approach you take, you have to enumerate all possible types. I have explored the above approach in my YIO library that you may want to browse. If taking that approuch, consider some code generator so that the number of macro overloads can be easily generated. You can use EVAL(EVAL(EVAL(...)))
macro way to overload a macro, but in my experience it results in completely unreadable very, very long error messages.
Anyway, C is just not really for generic programming. If you want to write generic functions with more fun, not with curses and constant pain, consider moving to another programming language with templating support. C++ is the closest to C and most prominent such language, but Rust, D, Ada and other such language may be of interest.
Upvotes: 1