For the past year or so, I've been discussing how to use the const and volatile qualifiers in declaring data. Using these qualifiers in data declarations causes a ripple that spills over into function declarations. The ripple effects are different in C++ than in C, largely because function declarations in C++ have more capabilities than they do in C. As groundwork for describing the ripple effects, I'd like to look at one of the primary differences between functions in C and C++.
Many C libraries contain at least one group of functions with very similar names. Each function in a group performs essentially the same operation but has a different parameter list or return type.
The Standard C library actually contains several such groups. For example, there's a group of “abs” functions that compute the absolute value of a single arithmetic operand. The group includes:
int abs(int i);long int labs(long int li);float fabsf(float f);double fabs(double d);long double fabsl(long double ld);
The functions fabsf and fabsl were not in the original C standard, but are in C9X, the revised standard in the works.
The Standard C library also has a group of “put” functions including:
int fputc(int c, FILE *f);int fputs(char const *s, FILE *f);int putchar(int c);int puts(char const *s);
All of these functions write something to a file; however, each writes a different type of thing to a file specified in a different way. The functions fputcand putchar write a single character (passed as an int), while puts and fputs write characters from a null-terminated string. The functions fputc and fputs write to the file passed as a parameter, while putchar and puts always write to standard output.
Even though the functions in a group have different names, programmers often refer to the functions as if they all had the same name. For example, I don't think I've ever heard any programmer say that a program “fputc-ed a character” or “fputs-ed a string.” Rather, they say the program “put a character” or “put a string.” We usually think of functions such as fputcand fputsas just put. We usually think of functions such as abs, labs, and the others as just abs. When we already understand that each function in a group is in fact a distinct function, there's little reason to belabor the distinction. In fact, belaboring the distinction can be more of a distraction.
It's often good programming style to use the same names in your source code that you use when you describe the program's behavior (either orally or in writing). The best name for each of several output functions in a program might be just plain “put.” Unfortunately, C doesn't give you the freedom to use the same name for two different functions in a program.
The demand for unique function names in C can be a burden for both the authors and users of libraries. The authors have to conjure up function names that are different, but not too different. Users have to learn the differences. A conscientious author can waste hours trying to devise a pattern for function name prefixes or suffixes that will make users' lives easier. Been there. Done that.
C++ avoids this particular nuisance by letting you overload function names. That is, you can use the same name for two or more functions in the same program. Function name overloading lets you write programs using function names that are, in a sense, more “natural.” Thus, programs that use overloading can be easier to read and write. (Of course, as with anything taken to excess, too much overloading can be detrimental to the code as well.)
In C++ you declare an overloaded function as you would any other function. It's just that an overloaded function has the same name as some other function. An overloaded function must have a parameter list that's different from all other functions with the same name so the compiler has some way to tell the functions apart.
For example, you can implement the output functions listed above as overloaded functions named put:
int put(int c); // putcharint put(int c, FILE *f); // fputcint put(char const *s); // putsint put(char const *s, FILE *f); // fputs
When the compiler encounters a call to a function named put, it selects the appropriate function to call by matching the type(s) of the actual argument( s) in the call against the type(s) of the formal parameter(s) in one of the declarations. The process of selecting the matching function is called overload resolution. For example, the following:
calls the put function declared as:
int put(int c, FILE *f);
int put(char const *s);
If the compiler can't find a function that will accept the specified arguments, it issues an error message. For example, calling:
put("Error #", n, stderr);
is an error because none of the put functions declared above accepts three arguments.
Although the number of formal parameters in the matching function declaration must be the same as the number of actual arguments in the call, the type of each parameter need not be exactly the same as the type of its corresponding argument. C++ compilers apply some promotions and conversions in an attempt to find a match.
For example, the functions declared as:
int put(int c);int put(int c, FILE *f);
write a single character to a file. In each function, parameter c has type int rather than char because the Standard C library uses the integer value EOF to represent end-of-file and I/O failures. A charmight not be able to represent EOF. An intcan represent all values of type char, as well as EOF.
Even though these function intas the parameter type, you can call them with char as the argument type. For example, given:
char c; ... put(c);
overload resolution selects:
int put(int c);
as the matching function. In this case, the compiler promotes char to int to achieve the match.
Best matches and ambiguities
More than one function in a group of overloaded functions might be capable of satisfying a particular function call. For example, of these four functions:
void f(int i);void f(long int li);void f(char *p);void f(double d, int i);
the first three are capable of satisfying a call to f(0). The call can pass 0as an int, as a long int, or even as a char * whose value is the null pointer. Clearly, the fourth function cannot satisfy the call because f(0) passes only one argument, while the fourth function requires two.
When confronted with a choice of functions, overload resolution uses a ranking of the conversions from the argument type into the parameter type to determine which function, if any, is the best match. For example, in the case of calling f(0), the argument 0has type int. Calling:
void f(char *p);
requires a pointer conversion (from int to char *), while calling:
void f(long int li);
requires an integral conversion (from intto long int). In contrast, the parameter type in:
void f(int i);
is an exact match for the argument type. An exact match is always preferable to a match that requires a conversion, and so this is the best matching function for the call f(0).
void f(int i);
had not been declared. In that case, overload resolution would have to choose between:
void f(long int li);void f(char *p);
as the best matching function for the call f(0). However, these conversions have the same rank—neither is the preferred conversion. When overload resolution cannot select one function as the unique best match, the call is ambiguous. An ambiguous call produces a compilation error.
There you have the basics of function name overloading. I'll be filling in the details over the coming months.
Dan Saks is the president of Saks & Associates, a C/C++ training and consulting company. He is also a contributing editor for the C/C++ Users Journal. He served for many years as secretary of the C++ standards committee and remains an active member. You can write to him at firstname.lastname@example.org.