Overloading Functions in C

来源:互联网 发布:阿里云服务器装oracle 编辑:程序博客网 时间:2024/05/21 10:24

文章来源http://locklessinc.com/articles/overloading/

It is well known that C++ allows one to overload functions, and C does not. This is typically done by "mangling" the name of a function, and thus including the types of its arguments in the symbol definition. Each variant of an overloaded function will then obtain a different symbolic name for the entry point. So by calling or jumping to the correct symbol we can execute the right version of the function.

Typically, the C compiler does no function symbol name mangling at all. (This is ignoring underscore hacks.) This means that you simply cannot define two versions of a function. They'll get the same symbolic name, and the linker will complain.

However, this isn't quite the whole story. Various parts of the C and POSIX standards require overloaded versions of C functions to exist. The simplest of these is the definition of the open() function. This function takes two arguments, the first of which describes the path of the file to open, and the second a set of flags describing exactly how to open the file. There is an optional third parameter that is only required when the file may be created. This third parameter describes the permissions settings to use for the new file.

So how do we specify an optional extra parameter in C code? This is a form of function overloading; overloading by number of parameters. Obviously it isn't done by mangling, and instead the va_args package can be used. An implementation of a function which takes only two arguments, with an optional third parameter only required when the second parameter takes a specific value is:

#include <stdio.h>#include <stdarg.h>void va_overload2(int p1, int p2){printf("va_overload2 %d %d\n", p1, p2);}void va_overload3(int p1, int p2, int p3){printf("va_overload3 %d %d %d\n", p1, p2, p3);}static void va_overload(int p1, int p2, ...){if (p2 == 7){va_list v;va_start(v, p2);int p3 = va_arg(v, int);va_end(v);va_overload3(p1, p2, p3);return;}va_overload2(p1, p2);}

In the above code, we can parse the arguments, and then choose to either call va_overload2() or va_overload3(). The POSIX open() function may have a similar implementation on your system.

The printf and sprintf family of functions can parse an arbitrary number of arguments. These use an initial format specifier to work out how many arguments, and of which type, exist. One problem with this technique is that a va_args based function doesn't do any type checking of its input arguments. This can be a source of errors if the program interprets one type as another. Fortunately, the gcc compiler provides a set of warnings for the standard functions that catch most of the problems. User functions can be annotated with special attributes that describe their input if it matches the format + args pattern.

Another common usage of the va_args package is to accept an unlimited number of arguments, with no direct specifier of the number to accept. By NULL-terminating the list, we can parse the arbitrary input to our function:

static void print_nt_strings(const char *s, ...){va_list v;va_start(v, s);/* Stop on NULL */while (s){printf("%s", s);/* Grab next parameter */s = va_arg(v, const char *);}va_end(v);}

The above function will print all the C-strings passed to it, no matter the number provided the last pointer is NULL. The problem here is remembering to add the final NULL to the list. If it is left off, the above function will interpret values on the stack as const char * pointers and try to print them out. This will invoke undefined behavior and probably crash the program.

A way to fix this problem is to explicitly say how many arguments exist, removing the need for the NULL on the end. Having the user manually specify the number is inconvenient and error prone. However, it is possible to automate this.

The first trick is to notice that the C pre-processor is powerful enough to count the number of arguments. We can then use it to calculate this number, and include it as a hidden parameter to the "real" version of the function.

#define COUNT_PARMS2(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _, ...) _#define COUNT_PARMS(...)\COUNT_PARMS2(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)static void print_strings2(int count, ...){int i;va_list v;va_start(v, count);for (i = 0; i < count; i++){/* Grab next parameter + print it */const char *s = va_arg(v, const char *);printf("%s", s);}va_end(v);}#define print_strings(...)\print_strings2(COUNT_PARMS(__VA_ARGS__), __VA_ARGS__)

By calling the print_strings() macro, we indirectly call the print_strings2() function with the first argument being the count of all the rest of the arguments passed.

Note that the above COUNT_PARMS magic macro will only work for up to 10 arguments. Extending it to more than 10 is relatively easy though. Another subtle problem is that the C pre-processor macros do not distinguish well between zero and one arguments. This makes it impossible to construct a macro to return "0", and thus call print_strings() with no arguments at all. With gcc will get the error "error: expected expression before ')' token". In this case it doesn't matter much, but in some cases it might make an API a bit more complex.

Using a little more C pre-processor magic, we can remove the need for va_args parsing for overloaded functions. The pre-processor can count the arguments, and then call a different function for each possibility. This requires pasting the number generated by the COUNT_PARMS() macro to the function name.

Some code which doesn't do the pasting would look like:

void count_overload1(int p1){printf("One param: %d\n", p1);}void count_overload2(double *p1, const char *p2){printf("Two params: %p (%f) %s\n", p1, *p1, p2);}void count_overload3(int p1, int p2, int p3){printf("Three params: %c %d %d\n", p1, p2, p3);}void count_overload_aux(int count, ...){va_list v;va_start(v, count);switch(count){case 1:{int p1 = va_arg(v, int);count_overload1(p1);break;}case 2:{double *p1 = va_arg(v, double *);const char *p2 = va_arg(v, const char *);count_overload2(p1, p2);break;}case 3:{int p1 = va_arg(v, int);int p2 = va_arg(v, int);int p3 = va_arg(v, int);count_overload3(p1, p2, p3);break;}default:{va_end(v);printf("Invalid arguments to function 'count_overload()'");exit(1);}}va_end(v);}#define count_overload(...)\count_overload_aux(COUNT_PARMS(__VA_ARGS__), __VA_ARGS__)

And some code which replaces count_overload_aux() with a macro is:

void cpp_overload1(int p1){printf("CPP One param: %d\n", p1);}void cpp_overload2(double *p1, const char *p2){printf("CPP Two params: %p (%f) %s\n", p1, *p1, p2);}void cpp_overload3(int p1, int p2, int p3){printf("CPP Three params: %c %d %d\n", p1, p2, p3);}#define CAT(A, B) CAT2(A, B)#define CAT2(A, B) A ## B#define cpp_overload(...)\CAT(cpp_overload, COUNT_PARMS(__VA_ARGS__))(__VA_ARGS__)

As can be seen, the second version is much simpler. It also can be quite a bit faster, as the compiler tends to find optimizing functions using va_args difficult. The C pre-processor does enough of the symbol mangling calculation that the compiler can even inline the correct version of the function in whatever calls it. Yet another advantage is that the compiler will check for mismatching argument count at compile time. There is no need for something corresponding to the message+exit default option in the switch statement in the first version.

Using the above macros it is possible to overload functions based on number of parameters provided that the overloaded function takes at least one argument. The actual types of the arguments are not inspected. This allows the code above to have different type signatures for each overload. Of course, this extra complexity is not always necessary, but it is nice to have the feature available.

Something similar to the above trick is that of default arguments to a function. Often we would like to have some parameters only changed if the function user requests it. Having default arguments that don't always need to be explicitly set can simplify interfaces. Simply by defining the less-argument versions of a function in terms of the full version, we can implement this in C.

#define cpp_default1(A) cpp_default2(A, "default string")void cpp_default2(int x, const char *s){printf("Got %d %s\n", x, s);}#define cpp_default(...)\CAT(cpp_default, COUNT_PARMS(__VA_ARGS__))(__VA_ARGS__)

The above code will in the default case print the integer first argument, followed by the "default string" string. If the user calls the function (macro) with two arguments, with the second of them a C string, that string will be used instead.

Another form of overloading is that due to type. This is done within the C standard library in some of the math functions. For example the isinf() function (which is actually a macro) will work with any floating point type. The macro calls the required version of the overloaded function by inspecting the type of the argument. This is done in the current version of glibc by investigating the size of the datatype.

void sizeof_overload_float(float f){printf("Got float %f\n", f);}void sizeof_overload_double(double d){printf("Got double %f\n", d);}void sizeof_overload_longdouble(long double ld){printf("Got long double %Lf\n", ld);}#define sizeof_overload(A)\((sizeof(A) == sizeof(float))?sizeof_overload_float(A):\(sizeof(A) == sizeof(double))?sizeof_overload_double(A):\(sizeof(A) == sizeof(long double))?sizeof_overload_longdouble(A):(void)0)

The above sizeof_overload() macro works similarly. It assumes it is passed a floating point type, and will print which one based on the size. However, there are a couple of flaws with this technique. The first is that not all types differ in size. The __float128 type has the same size as the long double type, and thus the wrong overload will be called. The gcc manual describes some intrinsics that can be used to fix this problem.

We can use __builtin_types_compatible_p() to compare types directly (instead of indirectly via their sizes), and use __builtin_choose_expr() instead of the unwieldy ternary operator.

struct s1{int a;int b;double c;};struct s2{long long a;long long b;};void gcc_overload_s1(struct s1 s){printf("Got a struct s1: %d %d %f\n", s.a, s.b, s.c);}void gcc_overload_s2(struct s2 s){printf("Got a struct s2: %lld %lld\n", s.a, s.b);}// warning: dereferencing type-punned pointer will break strict-aliasing rules#define gcc_overload(A)\__builtin_choose_expr(__builtin_types_compatible_p(typeof(A), struct s1),\gcc_overload_s1(*(struct s1 *)&A),\__builtin_choose_expr(__builtin_types_compatible_p(typeof(A), struct s2),\gcc_overload_s2(*(struct s2 *)&A),(void)0))

This allows us to overload on the two structs s1 and s2, even though they are the same size. The second problem is that we need to make sure that we call each overload with the correct type. We cannot for example call gcc_overload_s1() with an argument of type struct s2. This means that we need to add some type casts to the macro. Unfortunately, the compiler doesn't notice that the "wrong" case is never called, and will emit a warning about breaking aliasing rules. Unfortunately, silencing this warning via using a union causes incorrect code to be generated on gcc version 4.5 A final problem is that __builtin_types_compatible_p ignores type qualifiers such as const and volatile, making this technique not quite as powerful as C++ function overloads.

A more advanced method that doesn't have the aliasing problem is based on the fact that we can use macros to detect the type of the arguments. We can then pass this extra information to a function that then uses the va_args package to parse them. This suffers from one small issue. char and short typed arguments to variable argument C functions are expanded to type int. Similarlyfloat arguments are converted to double. Provided you don't need to determine between these particular types, we can overload on the type of any argument in a C function.

void gcc_type_overload_aux(int typeval, ...){switch(typeval){case 1:{va_list v;va_start(v, typeval);struct s1 s = va_arg(v, struct s1);va_end(v);gcc_overload_s1(s);break;}case 2:{va_list v;va_start(v, typeval);struct s2 s = va_arg(v, struct s2);va_end(v);gcc_overload_s2(s);break;}default:{printf("Invalid type to 'gcc_type_overload()'\n");exit(1);}}}#define gcc_type_overload(A)\gcc_type_overload_aux(\__builtin_types_compatible_p(typeof(A), struct s1) * 1\+ __builtin_types_compatible_p(typeof(A), struct s2) * 2\, A)

The above constructs an integer based on the type of the argument in the macro, and then uses a switch statement to parse it in the auxiliary function.

Taking the above to an extreme, we show a set of functions and macros that will describe the type and value of up to four arguments to a multiply overloaded function.

void print_type(int t, va_list *v){switch(t){case 1:{int p = va_arg(*v, int);printf("int :%d\n", p);break;}case 2:{long long p = va_arg(*v, long long);printf("long long :%lld\n", p);break;}case 3:{double p = va_arg(*v, double);printf("double :%f\n", p);break;}case 4:{long double p = va_arg(*v, long double);printf("long double :%Lf\n", p);break;}default:{printf("Unknown type\n");exit(1);}}}void param_lister1_aux(int t1, ...){va_list v;va_start(v, t1);printf("1st param:");print_type(t1, &v);va_end(v);}void param_lister2_aux(int t1, ...){int t2;va_list v;va_start(v, t1);printf("1st param:");print_type(t1, &v);t2 = va_arg(v, int);printf("2nd param:");print_type(t2, &v);va_end(v);}void param_lister3_aux(int t1, ...){int t2, t3;va_list v;va_start(v, t1);printf("1st param:");print_type(t1, &v);t2 = va_arg(v, int);printf("2nd param:");print_type(t2, &v);t3 = va_arg(v, int);printf("3rd param:");print_type(t3, &v);va_end(v);}void param_lister4_aux(int t1, ...){int t2, t3, t4;va_list v;va_start(v, t1);printf("1st param:");print_type(t1, &v);t2 = va_arg(v, int);printf("2nd param:");print_type(t2, &v);t3 = va_arg(v, int);printf("3rd param:");print_type(t3, &v);t4 = va_arg(v, int);printf("4th param:");print_type(t4, &v);va_end(v);}#define TYPENUM(A)\__builtin_types_compatible_p(typeof(A), int) * 1\+ __builtin_types_compatible_p(typeof(A), long long) * 2\+ __builtin_types_compatible_p(typeof(A), double) * 3\+ __builtin_types_compatible_p(typeof(A), long double) * 4#define param_lister1(A)\param_lister1_aux(TYPENUM(A), A)#define param_lister2(A, B)\param_lister2_aux(TYPENUM(A), A, TYPENUM(B), B)#define param_lister3(A, B, C)\param_lister3_aux(TYPENUM(A), A, TYPENUM(B), B, TYPENUM(C), C)#define param_lister4(A, B, C, D)\param_lister4_aux(TYPENUM(A), A, TYPENUM(B), B, TYPENUM(C), C, TYPENUM(D), D)#define param_lister(...)\CAT(param_lister, COUNT_PARMS(__VA_ARGS__))(__VA_ARGS__)

Thus, contrary to common knowledge, it is indeed possible to overload functions in C. Using standard C, the overload possibilities aren't quite as varied as those using gcc intrinsics. However, it is often extremely useful to overload on the number of function arguments alone. Type-based overloading with differently sized structs also works well with standard C. Using gcc intrinsics it is possible to obtain nearly the flexibility of C++ overloaded functions.

Comments

Dmitry V'jukov said...
Hi,
Nice tricks!
Do you have any idea how to achieve (note return values):
float sizeof_overload_float(float f);
double sizeof_overload_double(double d);
?
I am able to do that with gcc (with the help of __typeof__), but the problem is with MSVS C.
TIA
sfuerst said...
The above tricks don't care about the return types, so I set them all to be void for simplicity. You can alter them to be anything you wish. Your case looks exactly the case that is solved by the glibc method for the C math library overloaded function macros.

Changing the above code to add return values to the portable "sizeof" method gives:

float overload_float(float f)
{
    /* Use f */
    return f;
}

double overload_double(double d)
{
    /* Use d */
    return d;
}

#define overload(A)\
    ((sizeof(A) == sizeof(float))?overload_float(A):\
    (sizeof(A) == sizeof(double))?overload_double(A):(void)0)
degski said...
#define COUNT_PARMS(...) Y_TUPLE_SIZE_II((Y_TUPLE_SIZE_PREFIX_ ## __VA_ARGS__ ## _Y_TUPLE_SIZE_POSTFIX,32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0))
#define Y_TUPLE_SIZE_II(__args) Y_TUPLE_SIZE_I __args

#define Y_TUPLE_SIZE_PREFIX__Y_TUPLE_SIZE_POSTFIX ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,0

#define Y_TUPLE_SIZE_I(__p0,__p1,__p2,__p3,__p4,__p5,__p6,__p7,__p8,__p9,__p10,__p11,__p12,__p13,__p14,__p15,__p16,__p17,__p18,__p19,__p20,__p21,__p22,__p23,__p24,__p25,__p26,__p27,__p28,__p29,__p30,__p31,__n,...) __n

You signal that the argument counting is broken for 0 or 1, which I confirm.

The above is a solution to that problem.

The answer I found on this page http://stackoverflow.com/questions/2124339/c-preprocessor-va-args-number-of-arguments , posted by user: "user720594"

0 0
原创粉丝点击