So, we already know how to declare, define and use functions in programs. In this chapter, we'll talk about their special form - overloaded functions. Two functions are called overloaded if they have the same name, are declared in the same scope, but have different lists of formal parameters. We will explain how such functions are declared and why they are useful. Then we will consider the issue of their resolution, i.e. about which of the several overloaded functions is called during the execution of the program. This problem is one of the most difficult in C++. Those who want to get into the details will find it interesting to read the two sections at the end of the chapter, where the topic of argument type conversion and overload resolution is covered in more detail.

9.1. Overloaded function declarations

Now, having learned how to declare, define and use functions in programs, we will get acquainted with overload is another aspect in C++. Overloading allows you to have several functions of the same name that perform similar operations on arguments of different types.
You have already taken advantage of the predefined overloaded function. For example, to evaluate the expression

the operation of integer addition is called, while the evaluation of the expression

1.0 + 3.0

performs floating point addition. The choice of this or that operation is made imperceptibly for the user. The addition operator is overloaded to accommodate operands of different types. It is the responsibility of the compiler, not the programmer, to recognize the context and apply the operation appropriate to the types of the operands.
In this chapter, we will show you how to define your own overloaded functions.

9.1.1. Why you need to overload the function name

As with the built-in addition operation, we may need a set of functions that perform the same action, but on parameters of different types. Suppose we want to define functions that return the largest of the parameter values ​​passed in. If there were no overload, each such function would have to be given a unique name. For example, the max() family of functions might look like this:

int i_max(int, int); int vi_max(const vector &); int matrix_max(const matrix &);

However, they all do the same thing: they return the largest of the parameter values. From the user's point of view, there is only one operation here - the calculation of the maximum, and the details of its implementation are of little interest.
The noted lexical complexity reflects a programming environment limitation: any name occurring in the same scope must refer to a unique entity (object, function, class, etc.). Such a restriction in practice creates certain inconveniences, since the programmer must remember or somehow find all the names. Function overloading helps with this problem.
Using an overload, a programmer might write something like this:

int ix = max(j, k); vector vec; //... int iy = max(vec);

This approach proves to be extremely useful in many situations.

9.1.2. How to overload a function name

In C++, two or more functions can be given the same name, provided that their parameter lists differ either in the number of parameters or in their types. AT this example we declare an overloaded max() function:

intmax(int, int); int max(const vector &); int max(const matrix &);

Each overloaded declaration requires a separate max() function definition with an appropriate parameter list.
If a function name is declared more than once in some scope, then the second (and subsequent) declaration is interpreted by the compiler as follows:

  • if the parameter lists of two functions differ by number or parameter types, then functions are considered overloaded: // overloaded functions void print(const string &); void print(vector &);
  • if the return type and parameter lists in the declarations of two functions are the same, then the second declaration is considered repeated: // declarations of the same function void print(const string &str); void print(const string &); Parameter names are not taken into account when comparing declarations;
    if the parameter lists of two functions are the same, but the return types are different, then the second declaration is considered incorrect (inconsistent with the first) and is flagged by the compiler as an error: unsigned int max(int ​​i1, int i2); int max(int ​​i1, int i2);
    // error: only types differ
    // return values

Overloaded functions cannot differ only in their return types; if the parameter lists of two functions differ only in their default argument values, then the second declaration is considered repeated:

// declarations of the same function int max (int *ia, int sz); int max (int *ia, int = 10);

The typedef keyword creates an alternate name for existing type data, no new type is created. Therefore, if the parameter lists of two functions differ only in that one uses a typedef and the other uses a type for which typedef is an alias, the lists are considered the same, as in the following two declarations of the calc() function. In this case, the second declaration will give a compilation error because the return value is different from the one specified earlier:

// typedef does not introduce a new type typedef double DOLLAR; // error: same parameter lists, but different // return types extern DOLLAR calc(DOLLAR); extern int calc(double);

The const or volatile specifiers are not taken into account in such a comparison. Thus, the following two declarations are considered the same:

// declare the same function void f(int); void f(const int);

The const specifier is only important inside the function definition: it indicates that it is forbidden to change the value of the parameter in the function body. However, an argument passed by value can be used in the body of a function like a regular triggered variable: outside the function, the changes are not visible. (Methods of passing arguments, in particular passing by value, are discussed in Section 7.3.) Adding a const specifier to a parameter passed by value does not affect its interpretation. Any value of type int can be passed to a function declared as f(int), as can the function f(const int). Because they both take the same set of argument values, the above declarations are not considered overloaded. f() can be defined as

Void f(int i) ( )

Void f(const int i) ( )

The presence of these two definitions in one program is an error, since the same function is defined twice.
However, if a const or volatile specifier is applied to a parameter of a pointer or reference type, then it is taken into account when comparing declarations.

// different functions are declared void f(int*); void f(const int*); // and different functions are declared here
void f(int&);
void f(const int&);

9.1.3. When not to overload a function name

In what cases is name overloading not beneficial? For example, when assigning different names to functions makes the program easier to read. Here are some examples. The following functions operate on the same abstract date type. On the face of it, they are good candidates for overloading:

void setDate(Date&, int, int, int); Date &convertDate(const string &); void printDate(const Date&);

These functions operate on the same data type, the Date class, but perform semantically different actions. In this case, the lexical complexity associated with the use of different names stems from the programmer's convention of providing a set of operations on the data type and naming functions in accordance with the semantics of these operations. True, the C++ class mechanism makes such a convention redundant. We should make such functions members of the Date class, but leave different names that reflect the meaning of the operation:

#include class Date ( public: set(int, int, int); Date& convert(const string &); void print(); // ...
};

Let's take another example. The following five Screen member functions perform various operations on the screen cursor, which is a member of the same class. It may seem reasonable to overload these functions under the generic name move():

Screen& moveHome(); Screen& moveAbs(int, int); Screen& moveRel(int, int, char *direction); Screen& moveX(int); Screen& moveY(int);

However, the last two functions cannot be overloaded, since they have the same parameter lists. To make the signature unique, let's combine them into one function:

// function that combines moveX() and moveY() Screen& move(int, char xy);

Now all functions have different parameter lists, so they can be overloaded under the name move(). However, this should not be done: different names carry information without which the program will be more difficult to understand. For example, the cursor movement operations performed by these functions are different. For example, moveHome() performs a special kind of move to the top left corner of the screen. Which of the two calls below is more user-friendly and easier to remember?

// which call is clearer? myScreen.home(); // we think this one! myScreen.move();

In some cases, you don't need to overload the function name or assign different names: using default argument values ​​allows you to combine several functions into one. For example, cursor control functions

MoveAbs(int, int); moveAbs(int, int, char*);

differ in the presence of a third parameter of type char*. If their implementations are similar and a reasonable default value can be found for the third argument, then both functions can be replaced by one. In this case, a pointer with a value of 0 is suitable for the role of the default value:

Move(int, int, char* = 0);

You should use certain features when it is required by the application logic. It is not necessary to include overloaded functions in a program just because they exist.

9.1.4. Overloading and scope A

All overloaded functions are declared in the same scope. For example, a locally declared function does not overload, but simply hides the global one:

#include void print(const string &); voidprint(double); // overloads print() void fooBar(int ival)
{
// separate scope: hides both implementations of print()
extern void print(int); // error: print(const string &) is not visible in this area
print("Value: ");
print(ival); // correct: print(int) is visible
}

Since each class defines its own scope, functions that are members of two different classes do not overload each other. (Class member functions are covered in Chapter 13. Overload resolution for class member functions is covered in Chapter 15.)
It is also allowed to declare such functions inside the namespace. Each of them also has its own scope associated with it, so that functions declared in different scopes do not overload each other. For example:

#include namespace IBM ( extern void print(const string &); extern void print(double); // overloads print() ) namespace Disney ( // separate scope: // does not overload the print() function from the IBM namespace extern void print (int); )

The use of using declarations and using directives helps make namespace members available in other scopes. These mechanisms have some influence on the declarations of overloaded functions. (Using declarations and using directives were discussed in Section 8.6.)

How does using-declaration affect function overloading? Recall that it introduces an alias for a namespace member in the scope in which the declaration occurs. What do such declarations do in the following program?

Namespace libs_R_us ( int max(int, int); int max(double, double); extern void print(int);
extern void print(double);
) // using-declarations
using libs_R_us::max;
using libs_R_us::print(double); // error void func()
{
max(87, 65); // calls libs_R_us::max(int, int)
max(35.5, 76.6); // calls libs_R_us::max(double, double)

The first using declaration brings both libs_R_us::max functions into the global scope. Now any of the max() functions can be called inside func(). The types of the arguments determine which function to call. The second using declaration is a bug: it can't have a list of parameters. The libs_R_us::print() function is only declared like this:

Using libs_R_us::print;

The using declaration always makes available all overloaded functions with the specified name. This restriction ensures that the libs_R_us namespace interface is not violated. It is clear that in the case of a call

Print(88);

the namespace author expects the libs_R_us::print(int) function to be called. If you allow the user to selectively include only one of several overloaded functions in the scope, then the behavior of the program becomes unpredictable.
What happens if the using-declaration brings into scope a function with an already existing name? These functions look like they are declared right where the using declaration occurs. Therefore, the introduced functions participate in the process of resolving the names of all overloaded functions present in the given scope:

#include namespace libs_R_us ( extern void print(int); extern void print(double); ) extern void print(const string &); // libs_R_us::print(int) and libs_R_us::print(double)
// overload print(const string &)
using libs_R_us::print; void fooBar(int ival)
{
// print(const string &)
}

The using declaration adds two declarations to the global scope: one for print(int) and one for print(double). They are aliases in the libs_R_us space and are included in a set of overloaded functions named print where the global print(const string &) already exists. When resolving the print overload in fooBar, all three functions are considered.
If a using declaration introduces some function into a scope that already has a function with the same name and the same parameter list, it is considered an error. A using-declaration cannot alias a print(int) function in the libs_R_us namespace if print(int) already exists in the global scope. For example:

Namespace libs_R_us ( void print(int); void print(double); ) void print(int); using libs_R_us::print; // error: repeated declaration print(int) void fooBar(int ival)
{
print(ival); // which print? ::print or libs_R_us::print
}

We have shown how using-declarations and overloaded functions are related. Now let's look at the specifics of using the using-directive. The using directive causes namespace members to appear declared outside of that space, adding them to a new scope. If there is already a function with the same name in this scope, then an overload occurs. For example:

#include namespace libs_R_us ( extern void print(int); extern void print(double); ) extern void print(const string &); // using directive
// print(int), print(double) and print(const string &) are elements
// the same set of overloaded functions
using namespace libs_R_us; void fooBar(int ival)
{
print("Value: "); // calls the global function
// print(const string &)
print(ival); // calls libs_R_us::print(int)
}

This is also true when there are multiple using directives. Functions with the same name, which are members of different spaces, are included in the same set:

Namespace IBM ( int print(int); ) namespace Disney ( double print(double); ) // using-directive // ​​generates many overloaded functions from different // namespaces using namespace IBM; using namespace Disney; long double print(long double); int main() (
print(1); // called IBM::print(int)
print(3.1); // call Disney::print(double)
return 0;
}

The set of overloaded functions named print in the global scope includes the print(int), print(double), and print(long double) functions. All of them are considered in main() during overload resolution, although they were originally defined in different namespaces.
So, again, overloaded functions are in the same scope. In particular, they end up there as a result of using declarations and using directives that make names from other scopes available.

9.1.5. extern "C" directive and overloaded A functions

We saw in Section 7.7 that the extern "C" bind directive can be used in a C++ program to indicate that some object is in a C part. How does this directive affect overloaded function declarations? Can functions written in both C++ and C be in the same set?
A bind directive is allowed to specify only one of the many overloaded functions. For example, the following program is incorrect:

// error: directive specified for two overloaded functions extern "C" extern "C" void print(const char*); extern "C" void print(int);

The following example of an overloaded calc() function illustrates a typical use of the extern "C" directive:

ClassSmallInt(/* ... */); class BigNum(/* ... */); // a function written in C can be called both from a program,
// written in C or from a program written in C++.
// C++ functions handle parameters that are classes
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&);
extern BigNum calc(const BigNum&);

The calc() function written in C can be called both from C and from a C++ program. The other two functions take a class as a parameter and therefore can only be used in a C++ program. The order of the declarations is not significant.
The bind directive is irrelevant in deciding which function to call; only the parameter types are important. The function that best matches the types of the passed arguments is selected:

Smallint si = 8; int main() ( calc(34); // call C function calc(double) calc(si); // call C++ function calc(const SmallInt &) // ... return 0; )

9.1.6. Pointers to overloaded functions A

You can declare a pointer to one of the many overloaded functions. For example:

extern void ff(vector ); extern void ff(unsigned int); // what function does pf1 point to?
void (*pf1)(unsigned int) =

Since the ff() function is overloaded, the &ff initializer alone is not enough to select the correct option. To understand which function initializes the pointer, the compiler looks in the set of all overloaded functions for one that has the same return type and parameter list as the function referred to by the pointer. In our case, the ff(unsigned int) function will be selected.
But what if there is no function that exactly matches the type of the pointer? Then the compiler will give an error message:

extern void ff(vector ); extern void ff(unsigned int); // error: match not found: invalid parameter list void (*pf2)(int) = // error: match not found: wrong return type double (*pf3)(vector ) = &ff;

Assignment works in a similar way. If the value of the pointer should be the address of an overloaded function, then the type of the pointer to the function is used to select the operand on the right side of the assignment operator. And if the compiler does not find a function that exactly matches the desired type, it issues an error message. Thus, type conversion between function pointers is never performed.

Matrix calc(const matrix &); intcalc(int, int); int (*pc1)(int, int) = 0;
int (*pc2)(int, double) = 0; // ...
// correct: function calc(int, int) is selected
pc1 = // error: no match: invalid second parameter type
pc2=

9.1.7. Secure Binding A

When using overloading, one gets the impression that a program can have several functions of the same name with different lists of parameters. However, this lexical convenience exists only at the source text level. In most compilation systems, programs that process this text to produce executable code require that all names be distinct. Link editors usually allow external links lexically. If such an editor encounters the name print two or more times, it cannot distinguish them by type analysis (the type information is usually lost by this point). So it just prints a message about the redefined character print and exits.
To solve this problem, the function name, along with its parameter list, is decorated to give a unique internal name. Programs called after the compiler only see this internal name. How exactly this name resolution is done is implementation dependent. The general idea is to represent the number and types of parameters as a character string and append it to the function name.
As mentioned in section 8.2, such coding guarantees, in particular, that two declarations of functions with the same name with different parameter lists, located in different files, are not perceived by the linker as declarations of the same function. Since this method helps to distinguish between overloaded functions during the link editing phase, we are talking about secure linking.
Name decoration does not apply to functions declared with the extern "C" directive, since only one of the many overloaded functions can be written in pure C. Two functions with different parameter lists declared as extern "C" are interpreted by the linker as one and the same character.

Exercise 9.1

Why would you need to declare overloaded functions?

Exercise 9.2

How to declare overloaded versions of the error() function so that the following calls are correct:

Int index; int upperBound; char selectVal; // ... error("Array out of bounds: ", index, upperBound); error("Division by zero"); error("Invalid selection", selectVal);

Exercise 9.3

Explain the effect of the second declaration in each of the following examples:

(a) intcalc(int, int); int calc(const int, const int); (b) int get(); double get(); (c) int *reset(int *); double *reset(double *): (d) extern "C" int compute(int *, int); extern "C" double compute(double *, double);

Exercise 9.4

Which of the following initializations results in an error? Why?

(a) void reset(int *); void (*pf)(void *) = reset; (b) intcalc(int, int); int (*pf1)(int, int) = calc; (c) extern "C" int compute(int *, int); int (*pf3)(int*, int) = compute; (d) void (*pf4)(const matrix &) = 0;

9.2. Three steps of overload resolution

Function overload resolution called the process of choosing the function from the set of overloaded, which should be called. This process is based on the arguments specified when called. Consider an example:

T t1, t2; void f(int, int); void f(float, float); int main() (
f(t1, t2);
return 0;
}

Here, during the overload resolution process, depending on the type of T, it is determined whether the function f(int,int) or f(float,float) will be called when processing the expression f(t1,t2) or an error will be recorded.
Function overload resolution is one of the most complex aspects of the C++ language. Trying to understand all the details, novice programmers will face serious difficulties. Therefore, in this section, we present only short review of how overload resolution works, so that you get at least some impression of the process. For those who want to know more, the next two sections provide a more detailed description.
The process of resolving a function overload consists of three steps, which we will show in the following example:

void f(); void f(int); void f(double, double = 3.4); void f(char *, char *); void main() (
f(5.6);
return 0;
}

When resolving a function overload, the following steps are taken:

  1. The set of overloaded functions for a given call is highlighted, as well as the properties of the list of arguments passed to the function.
  2. Those of the overloaded functions that can be called with the given arguments are selected, taking into account their number and types.
  3. The function that best matches the call is found.

Let's consider each item in turn.
The first step is to identify the set of overloaded functions that will be considered in this call. The functions included in this set are called candidates. A candidate function is a function with the same name as the called one, and its declaration is visible at the point of the call. In our example, there are four such candidates: f(), f(int), f(double, double) and f(char*, char*).
After that, the properties of the list of passed arguments are identified, i.e. their number and types. In our example, the list consists of two double arguments.
At the second step, among the set of candidates, the viable ones are selected - those that can be called with the given arguments. The persistent function either has as many formal parameters as the actual arguments passed to the called function, or more, but then for each additional parameter, default value. For a function to be considered persistent, any actual argument passed in the call must have a conversion to the type of the formal parameter specified in the declaration.

In our example, there are two established functions that can be called with the given arguments:

  • the function f(int) survived because it has only one parameter and there is a conversion of the actual double argument to the formal int parameter;
  • the function f(double,double) survived because there is a default value for the second argument, and the first formal parameter is of type double, which is exactly the type of the actual argument.

If after the second step no stable functions were found, then the call is considered erroneous. In such cases, we say that there is a lack of correspondence.
The third step is to select the function that best suits the context of the call. Such a function is called the best standing (or best fit). At this step, the conversions used to cast the types of actual arguments to the types of the formal parameters of the established function are ranked. The most suitable function is considered to be the one for which the following conditions are met:
the transformations applied to the actual arguments are no worse than the transformations required to call any other well-established function;
for some arguments, the conversions applied are better than the conversions required to cast the same arguments in calls to other well-established functions.
Type conversions and their ranking are discussed in more detail in section 9.3. Here, we will only briefly look at ranking transforms for our example. For the established function f(int), the standard cast of the actual argument of type double to int must be applied. For the established function f(double,double) the type of the actual argument double exactly matches the type of the formal parameter. Since an exact match is better than a standard conversion (no conversion is always better than having one), f(double,double) is considered the most appropriate function for this call.
If at the third step it is not possible to find the only best of the established functions, in other words, there is no such established function that would fit more than all the others, then the call is considered ambiguous, i.e. erroneous.
(Section 9.4 discusses all the overload resolution steps in more detail. The resolution process is also used when calling an overloaded class member function and an overloaded operator. Section 15.10 discusses the overload resolution rules that apply to class member functions, and Section 15.11 discusses the rules for (overloaded operators. Overload resolution should also take into account functions instantiated from templates. Section 10.8 discusses how templates affect this resolution.)

Exercise 9.5

What happens in the last (third) step of the function overload resolution process?

9.3. Argument Type Conversions A

In the second step of the function overload resolution process, the compiler identifies and ranks the conversions that should be applied to each actual argument of the called function to convert it to the type of the corresponding formal parameter of any of the well-established functions. The ranking can give one of three possible outcomes:

  • exact match. The type of the actual argument exactly matches the type of the formal parameter. For example, if the set of overloaded print() functions has these: void print(unsigned int); void print(const char*); voidprint(char);
  • then each of the following three calls yields an exact match:
    unsigned int a;
print("a"); // matches print(char); print("a"); // matches print(const char*); print(a); // matches print(unsigned int);
  • matching with type conversion. The type of the actual argument does not match the type of the formal parameter, but can be converted to it: void ff(char); ff(0); // argument of type int is cast to type char
  • lack of compliance. The type of the actual argument cannot be cast to the type of the formal parameter in the function declaration because the required conversion does not exist. There is no match for each of the following two calls to the print() function:
  • // print() functions are declared as above int *ip; class SmallInt ( /* ... */ ); SmallInt si; print(ip); // error: no match
    print(si); // error: no match
  • To establish an exact match, the type of the actual argument does not have to match the type of the formal parameter. Some trivial transformations can be applied to the argument, namely:

    • converting l-value to r-value;
    • converting an array to a pointer;
    • converting a function to a pointer;
    • specifier conversions.

    (They are discussed in more detail below.) The category of conformance with type conversion is the most complex. There are several kinds of such a cast to consider: type extensions (promotions), standard conversions, and user-defined conversions. (Type extensions and standard conversions are discussed in this chapter. User-defined conversions will be introduced later, after classes have been discussed in detail; they are performed by the converter, a member function that allows you to define your own set of “standard” transformations in a class. In Chapter 15, we will look at such converters and how they affect function overload resolution.)
    When choosing the best well-established function for a given call, the compiler looks for the function for which the transformations applied to the actual arguments are the "best". Type conversions are ranked as follows: an exact match is better than a type extension, a type extension is better than a standard conversion, and this in turn is better than a user-defined conversion. We'll come back to ranking in Section 9.4, but for now simple examples Let's show how it helps to choose the most appropriate function.

    9.3.1. Learn more about Exact Match

    The simplest case occurs when the types of the actual arguments are the same as the types of the formal parameters. For example, there are two overloaded max() functions shown below. Then each of the calls to max() exactly matches one of the declarations:

    int max(int, int); double max(double, double); int i1; void calc(double d1) (
    max(56, i1); // exactly matches max(int, int);
    max(d1, 66.9); // exactly matches max(double, double);
    }

    An enumerated type exactly matches only those defined in it. enumeration elements, as well as objects that are declared to be of that type:

    Enum Tokens ( INLINE = 128; VIRTUAL = 129; ); Tokens curTok = INLINE; enum Stat( Fail, Pass ); extern void ff(Tokens);
    extern void ff(Stat);
    extern void ff(int); int main() (
    ff(Pass); // exactly matches ff(Stat)
    ff(0); // exactly matches ff(int)
    ff(curTok); // exactly matches ff(Tokens)
    // ...
    }

    It was mentioned above that the actual argument can exactly match the formal parameter, even if some trivial conversion is necessary to cast their types, the first of which is the conversion of l-value to r-value. An l-value is an object that satisfies the following conditions:

    • you can get the address of the object;
    • you can get the value of an object;
    • this value is easy to modify (unless there is a const specifier in the object declaration).

    In contrast, an r-value is an expression whose value is evaluated, or an expression denoting a temporary object for which an address cannot be obtained and whose value cannot be modified. Here is a simple example:

    intcalc(int); int main() ( int lval, res; lval = 5; // lvalue: lval; rvalue: 5
    res = calc(lval);
    // lvalue:res
    // rvalue: temporary object to store the value,
    // returned by the calc() function
    return 0;
    }

    In the first assignment statement, the variable lval is the l-value, and the literal 5 is the r-value. In the second assignment statement, res is the l-value, and the temporary object that stores the result returned by the calc() function is the r-value.
    In some situations, in a context where a value is expected, an expression that is an l-value can be used:

    intobj1; intobj2; int main() (
    // ...
    intlocal = obj1 + obj2;
    return 0;
    }

    Here obj1 and obj2 are l-values. However, to perform the addition in the main() function, their values ​​are extracted from the variables obj1 and obj2. The act of extracting the value of an object represented by an l-value expression is called converting an l-value to an r-value.
    When the function expects an argument passed by value, if the argument is an l-value, it is converted to an r-value:

    #include stringcolor("purple"); voidprint(string); int main() (
    print(color); // exact match: lvalue conversion
    // in rvalue
    return 0;
    }

    Since the argument in the print(color) call is passed by value, the l-value is converted to an r-value to extract the value of color and pass it to the print(string) prototype function. However, even though such a cast has taken place, the actual color argument is assumed to exactly match the print(string) declaration.
    When calling functions, it is not always necessary to apply such a conversion to arguments. The reference is an l-value; if the function has a reference parameter, then when the function is called, it receives an l-value. Therefore, the transformation described is not applied to the actual argument to which the formal reference parameter corresponds. For example, let's say the following function is declared:

    #include void print(list &);

    In the call below, li is the l-value representing the list object , passed to the print() function:

    List li(20); int main() (
    // ...
    print(li); // exact match: no conversion from lvalue to
    // rvalue
    return 0;
    }

    Matching li with a reference parameter is considered an exact match.
    The second conversion, which still fixes an exact match, is the conversion of an array to a pointer. As noted in Section 7.3, a function parameter is never an array type, transforming instead into a pointer to its first element. Similarly, an actual array type argument from NT (where N is the number of elements in the array and T is the type of each element) is always cast to a pointer to T. This type conversion of the actual argument is called array-to-pointer conversion. Despite this, the actual argument is considered to exactly match the formal parameter of type "pointer to T". For example:

    int ai; void putValues(int*); int main() (
    // ...
    putValues(ai); // exact match: convert array to
    // pointer
    return 0;
    }

    Before calling the putValues() function, the array is converted to a pointer, resulting in the actual argument ai (an array of three integers) being cast to a pointer to an int. Although the formal parameter of the putValues() function is a pointer and the actual argument is converted when called, an exact correspondence is established between them.
    When establishing an exact match, it is also possible to convert the function to a pointer. (It was mentioned in Section 7.9.) Like an array parameter, a function parameter becomes a function pointer. The actual argument of type "function" is also automatically cast to the type of a function pointer. This type conversion of the actual argument is called function-to-pointer conversion. While the transformation is performed, the actual argument is considered to exactly match the formal parameter. For example:

    Int lexicoCompare(const string &, const string &); typedef int (*PFI)(const string &, const string &);
    void sort(string *, string *, PFI); string as; int main()
    {
    // ...
    sort(as,
    as + sizeof(as)/sizeof(as - 1),
    lexicoCompare // exact match
    // convert function to pointer
    ); return 0;
    }

    Before the sort() call, a function-to-pointer conversion is applied, which casts the lexicoCompare argument from type "function" to type "pointer to function". Although the formal argument to the function is a pointer and the actual argument is the name of the function, and therefore the function has been converted to a pointer, the actual argument is assumed to be exactly the third formal parameter of the sort() function.
    The last of the above is the transformation of specifiers. It applies only to pointers and consists of adding const or volatile specifiers (or both) to the type that addresses the given pointer:

    Int a = ( 4454, 7864, 92, 421, 938 ); int*pi = a; bool is_equal(const int * , const int *); void func(int *parm) ( // exact match between pi and parm: conversion of specifiers
    if (is_equal(pi, parm))
    // ... return 0;
    }

    Before calling the is_equal() function, the actual arguments pi and parm are converted from type "pointer to int" to type "pointer to const int". This transformation consists of adding a const specifier to the addressed type, and therefore belongs to the category of specifier transformations. Even though the function expects to receive two pointers to a const int, and the actual arguments are pointers to an int, it is assumed that there is an exact match between the formal and actual parameters of the is_equal() function.
    Specifier conversion applies only to the type that the pointer is addressing. It is not used when the formal parameter has a const or volatile specifier but the actual argument does not.

    extern void takeCI(const int); int main() (
    int ii = ...;
    takeCI(ii); // specifier conversion is not applied
    return 0;
    }

    Although the formal parameter of the takeCI() function is of type const int, and it is called with an argument ii of type int, there is no specifier conversion: there is an exact match between the actual argument and the formal parameter.
    All of the above is also true for the case when the argument is a pointer and the const or volatile specifiers refer to that pointer:

    extern void init(int *const); extern int *pi; int main() (
    // ...
    init(pi); // specifier conversion is not applied
    return 0;
    }

    The const specifier in the formal parameter of the init() function refers to the pointer itself, not to the type it addresses. Therefore, the compiler does not take this specifier into account when parsing the conversions to be applied to the actual argument. No specifier conversion is applied to the pi argument: this argument and the formal parameter are considered to match exactly.
    The first three of these transformations (l-value to r-value, array-to-pointer, and function-to-pointer) are often referred to as l-value transformations. (We will see in Section 9.4 that although both l-value transformations and specifier transformations are in the category of transformations that do not violate exact matching, its degree is considered higher when only the first transformation is needed. In the next section, we will talk about this in some detail. .)
    An exact match can be forced using explicit type casting. For example, if there are two overloaded functions:

    extern void ff(int); extern void ff(void *);

    Ff(0xffbc); // call ff(int)

    will exactly match ff(int), even though the literal 0xffbc is written as a hexadecimal constant. The programmer can force the compiler to call the ff(void *) function by explicitly performing a cast operation:

    Ff(reinterpret_cast (0xffbc)); // call ff(void*)

    If such a cast is applied to the actual argument, then it acquires the type to which it is converted. Explicit type conversions help control the overload resolution process. For example, if overload resolution produces an ambiguous result (the actual arguments match two or more well-established functions equally well), then an explicit cast can be used to resolve the ambiguity, forcing the compiler to choose a particular function.

    9.3.2. More about type extension

    A type extension is one of the following conversions:

    • the actual argument of type char, unsigned char, or short is expanded to type int. The actual argument of type unsigned short is expanded to type int if the machine size of int is greater than the size of short, and to type unsigned int otherwise;
    • the argument of type float is expanded to type double;
    • an enumerated type argument expands to the first of the following types that is capable of representing all enum member values: int, unsigned int, long, unsigned long;
    • bool argument is expanded to int type.

    A similar extension is applied when the type of the actual argument is one of the types just listed, and the formal parameter is of the corresponding extended type:

    extern void manip(int); int main() (
    manip("a"); // type char expands to int
    return 0;
    }

    A character literal is of type char. It expands to int. Since the extended type corresponds to the type of the formal parameter of the manip() function, we say that calling it requires extending the argument type.
    Consider the following example:

    extern void print(unsigned int); extern void print(int); extern void print(char); unsigned char uc;
    print(uc); // print(int); uc only needs a type extension

    On a hardware platform where unsigned char takes one byte of memory and int takes four bytes, the extension converts unsigned char to int because it can represent all unsigned char values. For such a machine architecture, of the many overloaded functions shown in the example, print(int) provides the best match for an unsigned char argument. For the other two functions, matching requires a standard cast.
    The following example illustrates the expansion of an actual enumerated type argument:

    Enum Stat(Fail, Pass); extern void ff(int);
    extern void ff(char); int main() (
    // correct: enum member Pass expands to type int
    ff(Pass); // ff(int)
    ff(0); // ff(int)
    }

    Sometimes expanding enums brings surprises. Compilers often choose the representation of an enum based on the values ​​of its elements. Suppose the above architecture (one byte for char and four bytes for int) defines an enum like this:

    Enum e1 ( a1, b1, c1 );

    Since there are only three elements a1, b1 and c1 with values ​​0, 1 and 2 respectively - and since all these values ​​can be represented by type char, then the compiler will usually choose char to represent type e1. Consider, however, the enumeration e2 with the following set of elements:

    Enum e2 ( a2, b2, c2=0x80000000 );

    Since one of the constants has the value 0x80000000, the compiler must choose to represent e2 with a type that is sufficient to store the value 0x80000000, that is, unsigned int.
    So, although both e1 and e2 are enums, their representations are different. Because of this, e1 and e2 expand to different types:

    #include string format(int);
    string format(unsigned int); int main() (
    format(a1); // call format(int)
    format(a2); // call format(unsigned int)
    return 0;
    }

    The first time format() is called, the actual argument is expanded to type int because char is used to represent type e1, and therefore the overloaded format(int) function is called. On the second call, the type of the actual argument e2 is unsigned int and the argument is expanded to unsigned int, which causes the overloaded format(unsigned int) function to be called. Therefore, be aware that the behavior of two enums with respect to the overload resolution process can be different and depend on the values ​​of the elements that determine how the type expansion occurs.

    9.3.3. Learn more about standard conversion

    There are five types of standard transformations, namely:

    1. integer type conversions: casting from an integer type or enumeration to any other integer type (excluding transformations, which were categorized above as type extensions);
    2. floating-point type conversions: casting from any floating-point type to any other floating-point type (excluding transformations, which were categorized above as type extensions);
    3. conversions between an integer type and a floating point type: casting from any floating point type to any integer type or vice versa;
    4. pointer conversions: casting the integer value 0 to a pointer type or transforming a pointer of any type into a void* type;
    5. conversions to bool type: casting from any integer type, floating point type, enumerated type, or pointer type to bool type.

    Here are some examples:

    extern void print(void*); extern void print(double); int main() (
    int i;
    print(i); // matches print(double);
    // i undergoes the standard conversion from int to double
    print(&i); // matches print(void*);
    // &i undergoes standard conversion
    // from int* to void*
    return 0;
    }

    Conversions belonging to groups 1, 2, and 3 are potentially dangerous because the target type may not represent all the values ​​of the source type. For example, floats cannot adequately represent all int values. It is for this reason that the transformations included in these groups are categorized as standard transformations and not type extensions.

    int i; voidcalc(float); int main() ( calc(i); // standard conversion between an integer type and a // floating point type is potentially dangerous depending // on the value of i return 0; )

    When the calc() function is called, the standard conversion from the integer type int to the floating point type float is applied. Depending on the value of the variable i, it may not be possible to store it as a float without loss of precision.
    It is assumed that all standard changes require the same amount of work. For example, conversion from char to unsigned char has no higher priority than conversion from char to double. Proximity of types is not taken into account. If two established functions require a standard transformation of the actual argument to match, then the call is considered ambiguous and flagged as an error by the compiler. For example, given two overloaded functions:

    extern void manip(long); extern void manip(float);

    then the following call is ambiguous:

    int main() ( manip(3.14); // error: ambiguity // manip(float) is no better than manip(int) return 0; )

    The constant 3.14 is of type double. With the help of one or another standard conversion, a correspondence can be established with any of the overloaded functions. Since there are two transformations leading to the goal, the call is considered ambiguous. Neither transformation takes precedence over the other. The programmer can resolve the ambiguity either by explicit type casting:

    Manip(static_cast (3.14)); // manip(long)

    or by using a suffix denoting that the constant is of type float:

    Manip (3.14F)); // manip(float)

    Here are some more examples of ambiguous calls that are flagged as errors because they match multiple overloaded functions:

    extern void farith(unsigned int); extern void farith(float); int main() (
    // each of the following calls is ambiguous
    farith("a"); // argument is of type char
    farith(0); // argument is of type int
    farith(2ul); // argument is of type unsigned long
    farith(3.14159); // argument is of type double
    farith(true); // argument is of type bool
    }

    Standard pointer conversions are sometimes counter-intuitive. In particular, the value 0 is cast to a pointer to any type; the pointer thus obtained is called null. The value 0 can be represented as a constant expression of an integer type:

    void set(int*); int main() (
    // pointer conversion from 0 to int* applied to arguments
    // in both calls
    set(0L);
    set(0x00);
    return 0;
    }

    The constant expression 0L (value 0 of type long int) and the constant expression 0x00 (hexadecimal integer value 0) are of integer type and therefore can be converted to a null pointer of type int*.
    But since enums are not integer types, the element equal to 0 is not cast to a pointer type:

    Enum EN ( zr = 0 ); set(zr); // error: zr cannot be converted to type int*

    The call to the set() function is an error because there is no conversion between the value zr of the enumeration element and the formal parameter of type int*, even though zr is 0.
    Note that the constant expression 0 is of type int. To cast it to a pointer type, a standard conversion is required. If there is a function with a formal parameter of type int in the set of overloaded functions, then overloading will be allowed in its favor in the case when the actual argument is 0:

    Void print(int); voidprint(void*); void set(const char*);
    void set(char *); int main()(
    print(0); // called print(int);
    set(0); // ambiguity
    return 0;
    }

    A call to print(int) is an exact match, while a call to print(void*) requires casting the value 0 to a pointer type. Since a match is better than a transform, to resolve this call, one chooses print function(int). The call to set() is ambiguous, since 0 matches the formal parameters of both overloaded functions by applying the standard transformation. Since both functions are equally good, an ambiguity is fixed.
    The last possible pointer conversion allows you to cast a pointer of any type to type void*, since void* is a generic pointer to any data type. Here are some examples:

    #include extern void reset(void *); void func(int *pi, string *ps) (
    // ...
    reset(pi); // pointer conversion: int* to void*
    /// ...
    reset(ps); // pointer conversion: string* to void*
    }

    Only pointers to data types can be cast to void* using the standard conversion, pointers to functions cannot be done this way:

    Typedef int(*PFV)(); extern PFV test Cases; // array of pointers to functions extern void reset(void *); int main() (
    // ...
    reset(textcase); // error: no standard conversion
    // between int(*)() and void*
    return 0;
    }

    9.3.4. Links

    The actual argument or formal parameter of a function can be references. How does this affect type conversion rules?
    Consider what happens when the reference is an actual argument. Its type is never a reference type. The reference argument is treated as an l-value whose type is the same as the type of the corresponding object:

    int i; int& ri = i; voidprint(int); int main() (
    print(i); // argument is an lvalue of type int
    print(ri); // same
    return 0;
    }

    The actual argument in both calls is of type int. Using a reference to pass it in the second call does not affect the type of the argument itself.
    The standard type conversions and extensions considered by the compiler are the same when the actual argument is a reference to type T and when it is itself of that type. For example:

    int i; int& ri = i; voidcalc(double); int main() (
    calc(i); // standard conversion between integer type
    // and floating point type
    calc(ri); // same
    return 0;
    }

    And how does the formal reference parameter affect the transformations applied to the actual argument? The comparison gives the following results:

    • the actual argument is suitable as a reference parameter initializer. In this case, we say that there is an exact match between them: void swap(int &, int &); void manip(int i1, int i2) (
      // ...
      swap(i1, i2); // correct: call swap(int &, int &)
      // ...
      return 0;
      }
    • the actual argument cannot initialize the reference parameter. In such a situation, there is no exact match, and the argument cannot be used to call the function. For example:
    • int obj; void fred(double &); int main() ( frd(obj); // error: parameter must be of type const double & return 0; )
    • Calling the frd() function is an error. The actual argument is of type int and must be converted to type double to match the formal reference parameter. The result of this transformation is a temporary variable. Since a reference does not have a const specifier, such variables cannot be used to initialize it.
      Here is another example where there is no match between the formal reference parameter and the actual argument:
    • class B; void takeB(B&); BgiveB(); int main() (
      takeB(giveB()); // error: parameter must be of type const B &
      return 0;
      }
    • Calling the takeB() function is an error. The actual argument is the return value, i.e. a temporary variable that cannot be used to initialize a reference without the const specifier.
      In both cases, we see that if the formal reference parameter has the const specifier, then an exact match can be established between it and the actual argument.

    Note that both l-value to r-value conversion and reference initialization are considered exact matches. In this example, the first function call results in an error:

    Void print(int); voidprint(int&); intiobj;
    int &ri = iobj; int main() (
    print(iobj); // error: ambiguity
    print(ri); // error: ambiguity
    print(86); // correct: call print(int)
    return 0;
    }

    The iobj object is an argument that can be mapped to both print() functions, i.e. the call is ambiguous. The same applies to the next line, where the reference ri denotes the object corresponding to both print() functions. With the third call, however, everything is in order. For him, print(int&) is not well-established. An integer constant is an r-value, so it cannot initialize a reference parameter. The only established function to call print(86) is print(int), which is why it is chosen in overload resolution.
    In short, if the formal argument is a reference, then the actual argument is an exact match if it can initialize the reference, and not otherwise.

    Exercise 9.6

    Name two trivial transformations that are allowed when establishing an exact match.

    Exercise 9.7

    What is the rank of each of the argument conversions in the following function calls:

    (a) void print(int *, int); int arr; print(arr, 6); // function call (b) void manip(int, int); manip("a", "z"); // function call (c) int calc(int, int); double dobj; double = calc(55.4, dobj) // function call (d) void set(const int *); int*pi; set(pi); // function call

    Exercise 9.8

    Which of these calls are erroneous because there is no conversion between the type of the actual argument and the formal parameter:

    (a) enum Stat( Fail, Pass ); void test(Stat); text(0); // function call (b) void reset(void *); reset(0); // function call (c) void set(void *); int*pi; set(pi); // function call (d) #include list opera(); void print(oper()); // function call (e) void print(const int); intiobj; print(iobj); // function call

    9.4. Function overload resolution details

    We already mentioned in Section 9.2 that the function overload resolution process consists of three steps:

    1. Set a set of candidate functions to resolve a given call, as well as properties of the actual argument list.
    2. Select from a set of candidates the established functions - those that can be called with a given list of actual arguments, taking into account their number and types.
    3. Select the function that best fits the call by ranking the transformations to be applied to the actual arguments to match the formal parameters of the established function.

    We are now ready to explore these steps in more detail.

    9.4.1. Candidate Functions

    A candidate function is a function that has the same name as the one called. Candidates are selected in two ways:

    • the function declaration is visible at the point of the call. In the following example
      void f(); void f(int); void f(double, double = 3.4); void f(char*, char*); int main() (
      f(5.6); // there are four candidates to resolve this call
      return 0;
      }
    • all four f() functions satisfy this condition. Therefore, the set of candidates contains four elements;
    • if the type of the actual argument is declared inside some namespace, then member functions of this space that have the same name as the called function are added to the candidate set: namespace NS ( class C ( /* ... */ ); void takeC( C&); ) // type cobj is a class C declared in the NS namespace
      NS::Cobj; int main() (
      // none of the takeC() functions are visible at the call point
      takeC(cobj); // correct: NS::takeC(C&) is called,
      // because the argument is of type NS::C, so
      // the takeC() function is taken into account,
      // declared in NS namespace
      return 0;
      }

    Thus, the collection of candidates is the union of the set of functions visible at the call point and the set of functions declared in the same namespace as the actual argument types.
    When identifying the set of overloaded functions that are visible at the point of the call, the rules already discussed earlier apply.
    A function declared in a nested scope hides rather than overloads the function of the same name in the outer scope. In such a situation, only functions from within the nested scope will be candidates, i.e. those that are not hidden when called. In the following example, the candidate functions visible at the call point are format(double) and format(char*):

    Char* format(int); void g() ( char *format(double); char* format(char*); format(3); // calling format(double)
    }

    Because format(int) declared in the global scope is hidden, it is not included in the set of candidate functions.
    Candidates can be introduced using using declarations visible at the point of invocation:

    Namespace libs_R_us ( int max(int, int); double max(double, double); ) char max(char, char); void func()
    {
    // functions from the namespace are invisible
    // all three calls are resolved in favor of the global function max(char, char)
    max(87, 65);
    max(35.5, 76.6);
    max("J", "L");
    }

    The max() functions defined in the libs_R_us namespace are invisible at the call point. The only visible one is the max() function from the global scope; only it is included in the set of candidate functions and is called on each of the three calls to func (). We can use the using declaration to expose the max() functions from the libs_R_us namespace. Where to put the using declaration? If you include it in the global scope:

    Char max(char, char); using libs_R_us::max; // using-declaration

    then the max() functions from libs_R_us are added to the set of overloaded functions that already contains max() declared in the global scope. Now all three functions are visible inside func() and become candidates. In this situation, func() calls are resolved as follows:

    Void func() ( max(87, 65); // called libs_R_us::max(int, int) max("J", "L"); // called::max(char, char) )

    But what happens if we inject a using declaration into the local scope of the func() function, as shown in this example?

    void func() ( // using-declaration using libs_R_us::max; // same function calls as above
    }

    Which of the max() functions will be included in the candidate set? Recall that using declarations are nested within each other. With such a declaration in the local scope, the global function max(char, char) is hidden, so that only

    Libs_R_us::max(int, int); libs_R_us::max(double, double);

    They are the candidates. Now func() calls are resolved like this:

    Void func() ( // using-declaration // global function max(char, char) is hidden using libs_R_us::max; max(87, 65); // libs_R_us::max(int, int) is called
    max(35.5, 76.6); // libs_R_us::max(double, double) is called
    max("J", "L"); // call libs_R_us::max(int, int)
    }

    The using directives also affect the composition of the set of candidate functions. Suppose we decide to use them to make the max() functions from the libs_R_us namespace visible in func(). If we place the following using directive in the global scope, then the set of candidate functions will consist of the global function max(char, char) and the functions max(int, int) and max(double, double) declared in libs_R_us:

    Namespace libs_R_us ( int max(int, int); double max(double, double); ) char max(char, char);
    using namespace libs_R_us; // using directive void func()
    {
    max(87, 65); // call libs_R_us::max(int, int)
    max(35.5, 76.6); // libs_R_us::max(double, double) is called
    }

    What happens if you put the using directive in the local scope, as in the following example?

    Void func() ( // using-directive using namespace libs_R_us; // same function calls as above
    }

    Which of the max() functions will be among the candidates? Recall that a using directive makes namespace members visible as if they were declared outside that space, at the point where such a directive is placed. In our example, the members of libs_R_us are visible in the local scope of the func() function, as if they were declared out of space - in the global scope. It follows that the set of overloaded functions visible inside func() is the same as before, i.e. includes

    Max(char, char); libs_R_us::max(int, int); libs_R_us::max(double, double);

    A using-directive appears in the local or global scope, the resolution of calls to the func() function is not affected:

    Void func() ( using namespace libs_R_us; max(87, 65); // call libs_R_us::max(int, int)
    max(35.5, 76.6); // libs_R_us::max(double, double) is called
    max("J", "L"); // called::max(int, int)
    }

    So the set of candidates consists of functions visible at the call point, including those introduced by using-declarations and using-directives, as well as functions declared in namespaces associated with the actual argument types. For example:

    Namespace basicLib ( int print(int); double print(double); ) namespace matrixLib ( class matrix ( /* ... */ ); void print(const maxtrix &); ) void display() ( using basicLib::print ; matrixLib::matrix mObj;
    print(mObj); // call maxtrixLib::print(const maxtrix &) print(87); // basicLib::print(const maxtrix &) is called
    }

    Candidates for print(mObj) are the using-declarations inside display() of the basicLib::print(int) and basicLib::print(double) functions because they are visible at the call point. Since the actual function argument is of type matrixLib::matrix, the print() function declared in the matrixLib namespace would also be a candidate. What are the candidate functions for print(87)? Only basicLib::print(int) and basicLib::print(double) visible at the call point. Since the argument is of type int, no additional namespace is considered in the search for other candidates.

    9.4.2. Established features

    A well-established function is one of the candidates. The list of its formal parameters has either the same number of elements as the list of actual arguments of the called function, or more. In the latter case, for additional options default values ​​are given, otherwise the function cannot be called with the given number of arguments. For a function to be considered stable, there must be a conversion from each actual argument to the type of the corresponding formal parameter. (Such transformations were discussed in Section 9.3.)
    In the following example, the call to f(5.6) has two established functions: f(int) and f(double).

    void f(); void f(int); void f(double); void f(char*, char*); int main() (
    f(5.6); // 2 established functions: f(int) and f(double)
    return 0;
    }

    The f(int) function survived because it has only one formal parameter, which corresponds to the number of actual arguments in the call. In addition, there is a standard conversion of an argument of type double to int. The f(double) function also survived; it also has one parameter of type double, and it exactly matches the actual argument. The candidate functions f() and f(char*, char*) are excluded from the list of surviving functions because they cannot be called with a single argument.
    In the following example, the only established function to call format(3) is format(double). Although the format(char*) candidate can be called with a single argument, there is no conversion from the type of the actual int argument to the type of the formal parameter char*, and therefore the function cannot be considered well-established.

    Char* format(int); void g() ( // global function format(int) is hidden char* format(double); char* format(char*); format(3); // there is only one established function: format(double) )

    In the following example, all three candidate functions end up being able to call max() inside func(). All of them can be called with two arguments. Since the actual arguments are of type int, they correspond exactly to the formal parameters of the libs_R_us::max(int, int) function and can be cast to the parameter types of the libs_R_us::max(double, double) function by converting integers to floats, as well as to types libs_R_us::max(char, char) function parameters via integer type conversion.


    using libs_R_us::max; char max(char, char);
    void func()
    {
    // all three max() functions are well-established
    max(87, 65); // called using libs_R_us::max(int, int)
    }

    Note that a candidate function with multiple parameters is removed from standing as soon as it is found that one of the actual arguments cannot be cast to the type of the corresponding formal parameter, even though such a conversion exists for all other arguments. In the following example, the function min(char *, int) is excluded from the set of surviving ones, since it is not possible to convert the type of the first int argument to the type of the corresponding char * parameter. And this happens despite the fact that the second argument exactly matches the second parameter.

    extern double min(double, double); extern double min(char*, int); void func()
    {
    // one candidate function min(double, double)
    min(87, 65); // call min(double, double)
    }

    If, after excluding from the set of candidates all functions with an inappropriate number of parameters and those for whose parameters there was no suitable transformation, there are no standing ones, then the processing of the function call ends with a compilation error. In this case, it is said that no match was found.

    Void print(unsigned int); voidprint(char*); voidprint(char); int*ip;
    class SmallInt ( /* ... */ );
    SmallInt si; int main() (
    print(ip); // error: no established functions: no match found
    print(si); // error: no established functions: no match found
    return 0;
    }

    9.4.3. Best Established Feature

    The best is considered to be that of the established functions, the formal parameters of which most closely correspond to the types of the actual arguments. For any such function, the type conversions applied to each argument are ranked to determine how well it matches the parameter. (Section 6.2 describes supported type conversions.) The best-established function is a function that simultaneously satisfies two conditions:

    • the transformations applied to the arguments are no worse than the transformations required to call any other well-established function;
    • for at least one argument, the applied transformation is better than for the same argument in any other well-established function.

    It may happen that to cast the actual argument to the type of the corresponding formal parameter, you need to perform several conversions. So, in the following example

    Int arr; void putValues(const int *); int main() (
    putValues(arr); // 2 conversions needed
    // array to pointer + specifier conversion
    return 0;
    }

    to cast the argument arr from the type “array of three ints” to the type “pointer to const int”, a sequence of conversions is applied:

    1. Array to pointer conversion, which transforms an array of three ints into a pointer to int.
    2. Specifier conversion that transforms a pointer to int into a pointer to const int.

    Therefore, it would be more correct to say that in order to cast the actual argument to the type of the formal parameter of the established function, a sequence of conversions is required. Since not one, but multiple transforms are applied, the third step of the function overload resolution process actually ranks sequences of transforms.
    The rank of such a sequence is considered to be the rank of the worst of the transformations included in it. As explained in Section 9.2, type conversions rank as follows: an exact match is better than a type extension, and a type extension is better than a standard conversion. In the previous example, both changes have an exact match rank. Therefore, the entire sequence has the same rank.
    Such a collection consists of several transformations applied in the order shown:

    l-value conversion -> type extension or standard conversion -> specifier conversion

    The term l-value conversion refers to the first three of the exact match transformations discussed in Section 9.2: l-value to r-value conversion, array-to-pointer conversion, and function-to-pointer conversion. A sequence of transformations consists of zero or one l-value conversion, followed by zero or one type extension or standard conversion, and finally zero or one specifier conversion. Only one transformation of each kind can be applied to cast an actual argument to the type of a formal parameter.

    The described sequence is called the sequence of standard transformations. There is also a sequence user defined conversions, which is associated with a converter function that is a member of the class. (Converters and sequences of user-defined conversions are discussed in Chapter 15.)

    What are the sequences in which the actual arguments change in the following example?

    Namespace libs_R_us ( int max(int, int); double max(double, double); ) // using-declaration
    using libs_R_us::max; void func()
    {
    char c1, c2;
    max(c1, c2); // call libs_R_us::max(int, int)
    }

    The arguments to the max() function call are of type char. The sequence of argument transformations when calling the libs_R_us::max(int,int) function is as follows:

    1a. Since the arguments are passed by value, converting the l-value to an r-value extracts the values ​​of the arguments c1 and c2.

    2a. Arguments are converted from char to int using type expansion.
    The sequence of argument transformations when calling the libs_R_us::max(double,double) function is as follows:
    1b. By converting the l-value to an r-value, the values ​​of the arguments c1 and c2 are extracted.

    2b. The standard conversion between integer and float types casts arguments from type char to type double.

    The rank of the first sequence is a type extension (the worst change applied), while the rank of the second is a standard conversion. Since type extension is better than type conversion, the libs_R_us::max(int,int) function is chosen as the best fit for this call.
    If ranking sequences of argument transformations cannot reveal a single well-established function, then the call is considered ambiguous. In this example, both calls to calc() require the following sequence:

    1. Convert l-value to r-value to extract i and j argument values.
    2. A standard conversion for casting the actual arguments to the corresponding formal parameters.

    Since one cannot say which of these sequences is better than the other, the call is ambiguous:

    Int i, j; extern long calc(long, long); extern double calc(double, double); void jj() ( // error: ambiguity, no best match
    calc(i, j);
    }

    Specifier conversion (adding a const or volatile specifier to the type that addresses the pointer) has the rank of exact match. However, if two sequences of transformations differ only in that one of them has an additional specifier transformation at the end, then the sequence without it is considered better. For example:

    void reset(int *); void reset(const int *); int*pi; int main() (
    reset(pi); // without converting specifiers is better:
    // select reset(int *)
    return 0;
    }

    The sequence of standard conversions applied to the actual argument for the first candidate function reset(int*) is an exact match, it only takes going from l-value to r-value to extract the value of the argument. For the second candidate function reset(const int *) , the l-value to r-value transformation is also applied, but it is also followed by the specifier conversion to cast the resulting value from pointer to int to pointer to const int. Both sequences are an exact match, but there is no ambiguity. Since the second sequence differs from the first one by the presence of a specifier transformation at the end, the sequence without such a transformation is considered the best. Therefore, reset(int*) is the best standing function.
    Here is another example where casting specifiers affects which sequence is chosen:

    int extract(void*);
    int extract(const void *);

    int main() (
    extract(pi); // select extract(void *)
    return 0;
    }

    There are two established functions to call here: extract(void*) and extract(const void*). The sequence of transformations for the extract(void*) function consists of transforming the l-value into an r-value to extract argument values, followed by the standard pointer conversion: from a pointer to int to a pointer to void. For the extract(const void*) function, this sequence differs from the first one by an additional conversion of specifiers to cast the result type from a pointer to void to a pointer to const void. Since the sequences differ only by this transformation, the first one is chosen as more suitable and, therefore, the extract(const void*) function will be the best standing one.
    The const and volatile specifiers also affect the ranking of initialization of reference parameters. If two such initializations differ only in the addition of a const and volatile specifier, then the initialization without the extra specifier is considered better in overload resolution:

    #include void manip(vector &); void manip(const vector &); vector f();
    extern vector vec; int main() (
    manip(vec); // select manip(vector &)
    manip(f()); // select manip(const vector &)
    return 0;
    }

    In the first call, reference initialization for any function call is an exact match. But this challenge will still not be ambiguous. Since both initializations are the same except for the presence of an additional const specification in the second case, initialization without such a specification is considered better, so overloading will be resolved in favor of the well-established manip(vector &).
    For the second call, there is only one well-established function manip(const vector &). Since the actual argument is a temporary variable containing the result returned by f(), such an argument is an r-value that cannot be used to initialize the non-const formal reference parameter of manip(vector &). Therefore, the only well-established manip(const vector &).
    Of course, functions can have multiple actual arguments. The choice of the best of the standing ones should be made taking into account the ranking of the sequences of transformations of all arguments. Consider an example:

    extern int ff(char*, int); extern int ff(int, int); int main() ( ff(0, "a"); // ff(int, int)
    return 0;
    }

    The ff() function, which takes two int arguments, is chosen as the best standing function for the following reasons:

    1. her first argument is better. 0 matches exactly with a formal int parameter, while standard pointer conversion is required to match with a char * type parameter;
    2. its second argument has the same rank. To the argument "a" of type char, to establish a correspondence with the second formal parameter of any of the two functions, a sequence of conversions must be applied that has the rank of type extension.

    Here is another example:

    int compute(const int&, short); int compute(int&, double); extern intiobj;
    int main() (
    compute(iobj, "c"); // compute(int&, double)
    return 0;
    }

    Both functions compute(const int&, short) and compute(int&, double) survived. The second is chosen as the best for the following reasons:

    1. her first argument is better. Reference initialization for the first established function is worse because it requires the addition of a const specifier, which is not needed for the second function;
    2. its second argument has the same rank. To the argument "c" of type char, in order to establish a correspondence with the second formal parameter of any of the two functions, a sequence of transformations having the rank of a standard transformation must be applied.

    9.4.4. Arguments with default values

    Having arguments with default values ​​can extend a lot of well-established functions. Residual are functions that are called with a given list of actual arguments. But such a function can have more formal parameters than the actual arguments given, in the case where there is some default value for each parameter not specified:

    extern void ff(int); extern void ff(long, int = 0); int main() (
    ff(2L); // matches ff(long, 0); ff(0, 0); // matches ff(long, int);
    ff(0); // matches ff(int);
    ff(3.14); // error: ambiguity
    }

    For the first and third calls, the ff() function is settled, even though only one actual argument was passed. This is due to the following reasons:

    1. there is a default value for the second formal parameter;
    2. the first parameter of type long corresponds exactly to the actual argument in the first call, and can be cast to the type of the argument in the third call by a sequence that has the rank of a standard conversion.

    The last call is ambiguous because both established functions can be selected by applying the standard transformation to the first argument. The ff(int) function is not preferred just because it has one parameter.

    Exercise 9.9

    Explain what happens when you resolve an overload to call compute() inside main(). What features are candidates? Which of them will stand after the first step? What sequence of transformations must be applied to the actual argument so that it corresponds to the formal parameter for each established function? Which function will be the best standing?

    Namespace primerLib ( void compute(); void compute(const void *); ) using primerLib::compute;
    void compute(int);
    void compute(double, double = 3.4);
    void compute(char*, char* = 0); int main() (
    compute(0);
    return 0;
    }

    What happens if the using declaration is placed inside main() before calling compute()? Answer the same questions.

    Function overloading is the definition of several functions (two or more) with the same name but different parameters. The parameter sets of overloaded functions may differ in order, number, and type. Thus, function overloading is needed in order to avoid duplicating the names of functions that perform similar actions, but with different program logic. For example, consider the areaRectangle() function, which calculates the area of ​​a rectangle.

    Float areaRectangle(float, float) //function that calculates the area of ​​a rectangle with two parameters a(cm) and b(cm) ( return a * b; // multiply the lengths of the sides of the rectangle and return the resulting product )

    So, this is a function with two float type parameters, and the arguments passed to the function must be in centimeters, the return value of the float type is also in centimeters.

    Suppose that our initial data (rectangle sides) are given in meters and centimeters, for example: a = 2m 35 cm; b = 1m 86 cm. In this case, it would be convenient to use a function with four parameters. That is, each length of the sides of the rectangle is passed to the function in two parameters: meters and centimeters.

    Float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // function that calculates the area of ​​a rectangle with 4 parameters a(m) a(cm); b(m) b(cm) ( return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); )

    In the body of the function, the values ​​that were passed in meters (a_m and b_m) are converted to centimeters and added to the values ​​a_sm b_sm , after which we multiply the sums and get the area of ​​the rectangle in cm. Of course, it was possible to convert the original data into centimeters and use the first function, but now is not about that.

    Now, the most important thing is that we have two functions, with different signatures, but the same names (overloaded functions). A signature is a combination of a function name with its parameters. How to call these functions? And calling overloaded functions is no different from calling regular functions, for example:

    AreaRectangle(32, 43); // a function will be called that calculates the area of ​​a rectangle with two parameters a(cm) and b(cm) areaRectangle(4, 43, 2, 12); // a function will be called that calculates the area of ​​a rectangle with 4 parameters a(m) a(cm); b(m) b(cm)

    As you can see, the compiler will choose desired function, analyzing only the signatures of overloaded functions. Bypassing function overloading, one could simply declare a function with a different name, and it would do its job well. But imagine what will happen if you need more than two such functions, for example 10. And for each you need to come up with a meaningful name, and the hardest thing to remember is them. That's exactly why it's easier and better to overload functions, unless of course there is a need for this. Source programs are shown below.

    #include "stdafx.h" #include << "S1 = " << areaRectangle(32,43) << endl; // вызов перегруженной функции 1 cout << "S2 = " << areaRectangle(4, 43, 2, 12) << endl; // вызов перегруженной функции 2 return 0; } // перегруженная функция 1 float areaRectangle(float a, float b) //функция, вычисляющая площадь прямоугольника с двумя параметрами a(см) и b(см) { return a * b; // умножаем длинны сторон прямоугольника и возвращаем полученное произведение } // перегруженная функция 2 float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // функция, вычисляющая площадь прямоугольника с 4-мя параметрами a(м) a(см); b(м) b(cм) { return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); }

    // code Code::Blocks

    // Dev-C++ code

    #include using namespace std; // prototypes of overloaded functions float areaRectangle(float a, float b); float areaRectangle(float a_m, float a_sm, float b_m, float b_sm); int main() ( cout<< "S1 = " << areaRectangle(32,43) << endl; // вызов перегруженной функции 1 cout << "S2 = " << areaRectangle(4, 43, 2, 12) << endl; // вызов перегруженной функции 2 return 0; } // перегруженная функция 1 float areaRectangle(float a, float b) //функция, вычисляющая площадь прямоугольника с двумя параметрами a(см) и b(см) { return a * b; // умножаем длинны сторон прямоугольника и возвращаем полученное произведение } // перегруженная функция 2 float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // функция, вычисляющая площадь прямоугольника с 4-мя параметрами a(м) a(см); b(м) b(cм) { return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); }

    The result of the program is shown in Figure 1.

    Function overloading

    Overloading of operations (operators, functions, procedures)- in programming - one of the ways to implement polymorphism, which consists in the possibility of simultaneous existence in one scope of several different variants of an operation (operator, function or procedure) that have the same name, but differ in the types of parameters to which they are applied.

    Terminology

    The term "overloading" is a tracing paper of the English "overloading", which appeared in Russian translations of books on programming languages ​​in the first half of the 1990s. Perhaps this is not the best translation option, since the word "overload" in Russian has an established meaning of its own, radically different from the newly proposed one, however, it has taken root and is quite widespread. In the publications of the Soviet era, similar mechanisms were called in Russian “redefinition” or “redefinition” of operations, but this option is not disputable: discrepancies and confusion arise in the translations of the English “override”, “overload” and “redefine”.

    Reasons for the appearance

    Most early programming languages ​​had a restriction that no more than one operation with the same name could be available in a program at the same time. Accordingly, all functions and procedures visible at a given point in the program must have different names. The names and designations of functions, procedures, and operators that are part of the programming language cannot be used by the programmer to name his own functions, procedures, and operators. In some cases, a programmer can create his own program object with the name of another already existing one, but then the newly created object “overlaps” the previous one, and it becomes impossible to use both options at the same time.

    This situation is inconvenient in some fairly common cases.

    • Sometimes there is a need to describe and apply operations to data types created by the programmer that are equivalent in meaning to those already available in the language. A classic example is a library for working with complex numbers. They, like ordinary numeric types, support arithmetic operations, and it would be natural to create for this type of operation “plus”, “minus”, “multiply”, “divide”, denoting them with the same operation signs as for other numeric types. The ban on the use of elements defined in the language forces the creation of many functions with names like ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat, and so on.
    • When operations of the same meaning are applied to operands of different types, they are forced to be named differently. The inability to use functions with the same name for different types of functions leads to the need to invent different names for the same thing, which creates confusion and may even lead to errors. For example, in the classical C language, there are two versions of the standard library function for finding the modulus of a number: abs() and fabs() - the first is intended for an integer argument, the second for a real one. This situation, combined with weak C type checking, can lead to a hard-to-find error: if a programmer writes abs(x) in the calculation, where x is a real variable, then some compilers will generate code without warning that will convert x to an integer by discarding the fractional parts and calculate the modulus from the resulting integer!

    In part, the problem is solved by means of object programming - when new data types are declared as classes, operations on them can be formalized as class methods, including class methods of the same name (since methods of different classes do not have to have different names), but, firstly, such a design way of operations on values ​​of different types is inconvenient, and secondly, it does not solve the problem of creating new operators.

    Operator overloading itself is just "syntactic sugar", although even as such it can be useful because it allows the developer to program in a more natural way and makes custom types behave more like built-in ones. If we approach the issue from a more general position, then we can see that the tools that allow you to expand the language, supplement it with new operations and syntactic constructions (and overloading of operations is one of such tools, along with objects, macros, functionals, closures) turn it already in the metalanguage - a means of describing languages ​​oriented to specific tasks. With its help, it is possible to build a language extension for each specific task that is most appropriate for it, which will allow describing its solution in the most natural, understandable and simple form. For example, in an application to overloading operations: creating a library of complex mathematical types (vectors, matrices) and describing operations with them in a natural, “mathematical” form, creates a “language for vector operations”, in which the complexity of calculations is hidden, and it is possible to describe the solution of problems in terms of vector and matrix operations, focusing on the essence of the problem, not on the technique. It was for these reasons that such means were once included in the Algol-68 language.

    Overload mechanism

    Implementation

    Operator overloading involves the introduction of two interrelated features into the language: the ability to declare several procedures or functions with the same name in the same scope, and the ability to describe your own implementations of operations (that is, the signs of operations, usually written in infix notation, between operands). Basically, their implementation is quite simple:

    • To allow the existence of several operations of the same name, it is enough to introduce a rule into the language, according to which an operation (procedure, function, or operator) is recognized by the compiler not only by name (notation), but also by the types of their parameters. So abs(i), where i is declared as an integer, and abs(x), where x is declared as real, are two different operations. Fundamentally, there are no difficulties in providing just such an interpretation.
    • To enable defining and redefining operations, it is necessary to introduce appropriate syntactic constructions into the language. There can be quite a lot of options, but in fact they do not differ from each other, it is enough to remember that the entry of the form “<операнд1> <знакОперации> <операнд2>» is fundamentally similar to calling the function «<знакОперации>(<операнд1>,<операнд2>)". It is enough to allow the programmer to describe the behavior of operators in the form of functions - and the problem of description is solved.

    Options and problems

    Overloading procedures and functions at the level of a general idea, as a rule, is not difficult either to implement or to understand. However, even in it there are some "pitfalls" that must be considered. Allowing operator overloading creates a lot more problems for both the language implementer and the programmer working in that language.

    Identification problem

    The first question that a developer of a language translator that allows overloading of procedures and functions faces is: how to choose from among the procedures of the same name the one that should be applied in this particular case? Everything is fine if there is a variant of the procedure, the types of formal parameters of which exactly match the types of the actual parameters used in this call. However, in almost all languages, there is some degree of freedom in the use of types, assuming that the compiler automatically performs type-safe conversions in certain situations. For example, in arithmetic operations on real and integer arguments, integer is usually converted to real type automatically, and the result is real. Suppose there are two variants of the add function:

    int add(int a1, int a2); float add(float a1, float a2);

    How should the compiler handle the expression y = add(x, i) where x is a float and i is an int? Obviously there is no exact match. There are two options: either y=add_int((int)x,i) , or as y=add_flt(x, (float)i) (here the names add_int and add_float denote the first and second versions of the function, respectively).

    The question arises: should the compiler allow this use of overloaded functions, and if so, on what basis will it choose the particular variant used? In particular, in the example above, should the translator consider the type of the variable y when choosing? It should be noted that the above situation is the simplest, much more complicated cases are possible, which are aggravated by the fact that not only built-in types can be converted according to the rules of the language, but also classes declared by the programmer, if they have kinship relations, can be cast one to another. There are two solutions to this problem:

    • Prohibit inaccurate identification at all. Require that for each particular pair of types there is an exactly suitable variant of the overloaded procedure or operation. If there is no such option, the compiler should throw an error. The programmer in this case must apply an explicit conversion to cast the actual parameters to the desired set of types. This approach is inconvenient in languages ​​such as C++, which allow a fair amount of freedom in dealing with types, since it leads to a significant difference in the behavior of built-in and overloaded operators (arithmetic operations can be applied to ordinary numbers without thinking, but to other types - only with explicit conversion) or to the emergence of a huge number of options for operations.
    • Establish certain rules for choosing the “nearest fit”. Usually, in this variant, the compiler chooses those of the variants whose calls can be obtained from the source only by safe (non-lossy information) type conversions, and if there are several of them, it can choose based on which variant requires fewer such conversions. If the result leaves more than one possibility, the compiler throws an error and requires the programmer to explicitly specify the variant.

    Operation Overloading Specific Considerations

    Unlike procedures and functions, infix operations of programming languages ​​have two additional properties that significantly affect their functionality: priority and associativity, the presence of which is due to the possibility of "chain" recording of operators (how to understand a + b * c: as (a + b )*c or like a+(b*c) ? The expression a-b+c is (a-b)+c or a-(b+c) ?).

    The operations built into the language always have predefined traditional precedence and associativity. The question arises: what priorities and associativity will the redefined versions of these operations have, or, moreover, the new operations created by the programmer? There are other subtleties that may require clarification. For example, in C there are two forms of increment and decrement operators ++ and -- - prefix and postfix, which behave differently. How should the overloaded versions of such operators behave?

    Different languages ​​deal with these issues in different ways. Thus, in C++, the precedence and associativity of overloaded versions of operators is kept the same as those defined in the language; it is possible to separately overload the prefix and postfix forms of the increment and decrement operators using special signatures:

    So int is used to make a difference in signatures

    Announcement of new operations

    The situation with the announcement of new operations is even more complicated. Including the possibility of such a declaration in the language is not difficult, but its implementation is fraught with significant difficulties. Declaring a new operation is, in fact, creating a new programming language keyword, complicated by the fact that operations in text can usually follow other tokens without delimiters. When they appear, additional difficulties arise in the organization of the lexical analyzer. For example, if the language already has the operations “+” and the unary “-” (change of sign), then the expression a+-b can be accurately interpreted as a + (-b) , but if a new operation +- is declared in the program, it immediately arises ambiguity, because the same expression can already be parsed as a (+-) b . The developer and implementer of the language must deal with such problems in some way. The options, again, can be different: require that all new operations be single-character, postulate that in case of any discrepancies, the “longest” version of the operation is chosen (that is, until the next set of characters read by the translator matches any operation, it continues to be read), try to detect collisions during translation and generate errors in controversial cases ... One way or another, languages ​​that allow the declaration of new operations solve these problems.

    It should not be forgotten that for new operations there is also the issue of determining associativity and priority. There is no longer a ready-made solution in the form of a standard language operation, and usually you just have to set these parameters with the rules of the language. For example, make all new operations left-associative and give them the same, fixed, priority, or introduce into the language the means of specifying both.

    Overloading and polymorphic variables

    When overloaded operators, functions, and procedures are used in strongly typed languages, where each variable has a pre-declared type, it is up to the compiler to decide which version of the overloaded operator to use in each particular case, no matter how complex. This means that for compiled languages, the use of operator overloading does not lead to performance degradation - in any case, there is a well-defined operation or function call in the object code of the program. The situation is different when it is possible to use polymorphic variables in the language, that is, variables that can contain values ​​of different types at different times.

    Since the type of the value to which the overloaded operation will be applied is not known at the time of translation of the code, the compiler is deprived of the ability to choose the right option in advance. In this case, it is forced to embed a fragment in the object code that, immediately before performing this operation, will determine the types of the values ​​in the arguments and dynamically select a variant corresponding to this set of types. Moreover, such a definition must be made at each execution of the operation, because even the same code, being called a second time, may well be executed differently.

    Thus, the use of operator overloading in combination with polymorphic variables makes it inevitable to dynamically determine which code to call.

    Criticism

    The use of overload is not considered a boon by all experts. If function and procedure overloading is generally not objectionable (partly because it doesn't lead to some typical "operator" problems, partly because it's less tempting to misuse it), then operator overloading is, in principle, , and in specific language implementations, is subjected to quite severe criticism from many programming theorists and practitioners.

    Critics point out that the problems of identification, precedence, and associativity outlined above often make dealing with overloaded operators either unnecessarily difficult or unnatural:

    • Identification. If the language has strict identification rules, then the programmer is forced to remember for which combinations of types there are overloaded operations and manually cast operands to them. If the language allows "approximate" identification, one can never be sure that in some rather complicated situation, exactly the variant of the operation that the programmer had in mind will be performed.
    • Priority and associativity. If they are rigidly defined, this may be inconvenient and not relevant to the subject area (for example, for operations with sets, priorities differ from arithmetic ones). If they can be set by the programmer, this becomes an additional source of errors (if only because different variants of one operation turn out to have different priorities, or even associativity).

    How much the convenience of using your own operations can outweigh the inconvenience of deteriorating the controllability of the program is a question that does not have a clear answer.

    From the point of view of the language implementation, the same problems lead to the complexity of translators and the decrease in their efficiency and reliability. And the use of overloading in conjunction with polymorphic variables is also obviously slower than calling a hardcoded operation during compilation, and provides fewer opportunities for optimizing the object code. Specific features of the implementation of overloading in various languages ​​are subjected to separate criticism. So, in C++, the object of criticism can be the lack of an agreement on the internal representation of the names of overloaded functions, which gives rise to incompatibility at the level of libraries compiled by different C++ compilers.

    Some critics speak out against overloading operations, based on the general principles of software development theory and real industrial practice.

    • Proponents of the "puritan" approach to language building, such as Wirth or Hoare, oppose operator overloading simply because it can be easily dispensed with. In their opinion, such tools only complicate the language and the translator, without providing additional features corresponding to this complication. In their opinion, the very idea of ​​creating a task-oriented extension of the language only looks attractive. In reality, the use of language extension tools makes the program understandable only to its author - the one who developed this extension. The program becomes much more difficult for other programmers to understand and analyze, making maintenance, modification, and team development more difficult.
    • It is noted that the very possibility of using overload often plays a provocative role: programmers start using it wherever possible, as a result, a tool designed to simplify and streamline the program becomes the cause of its complication and confusion.
    • Overloaded operators may not do exactly what is expected of them, based on their kind. For example, a + b usually (but not always) means the same thing as b + a , but "one" + "two" is different from "two" + "one" in languages ​​where the + operator is overloaded for string concatenation.
    • Operator overloading makes program fragments more context-sensitive. Without knowing the types of the operands involved in an expression, it is impossible to understand what the expression does if it uses overloaded operators. For example, in a C++ program, the statement<< может означать и побитовый сдвиг, и вывод в поток. Выражение a << 1 возвращает результат побитового сдвига значения a на один бит влево, если a - целая переменная, но если a является выходным потоком , то же выражение выведет в этот поток строку «1» .

    Classification

    The following is a classification of some programming languages ​​according to whether they allow operator overloading, and whether operators are limited to a predefined set:

    Operations No overload There is an overload
    Limited set of operations
    • Objective-C
    • Python
    It is possible to define new operations
    • PostgreSQL
    • see also

      Wikimedia Foundation. 2010 .

      See what "Function Overloading" is in other dictionaries:

        - (operators, functions, procedures) in programming one of the ways to implement polymorphism, which consists in the possibility of simultaneous existence in one scope of several different variants of an operation (operator, function or ... ... Wikipedia



    How to achieve function overloading in C? (ten)

    Is there a way to achieve function overloading in C? I'm looking at simple functions that can be overloaded like

    foo (int a) foo (char b) foo (float c , int d)

    I think there is no direct way; I'm looking for workarounds, if any exist.

    I hope the below code will help you understand function overloading

    #include #include int fun(int a, ...); int main(int argc, char *argv)( fun(1,10); fun(2,"cquestionbank"); return 0; ) int fun(int a, ...)( va_list vl; va_start(vl,a ); if(a==1) printf("%d",va_arg(vl,int)); else printf("\n%s",va_arg(vl,char *)); )

    I mean, you mean - no, you can't.

    You can declare the va_arg function as

    void my_func(char* format, ...);

    But you will need to pass some information about the number of variables and their types in the first argument - like printf() .

    Yes, like.

    Here you give an example:

    void printA(int a)( printf("Hello world from printA: %d\n",a); ) void printB(const char *buff)( printf("Hello world from printB: %s\n",buff) ; ) #define Max_ITEMS() 6, 5, 4, 3, 2, 1, 0 #define __VA_ARG_N(_1, _2, _3, _4, _5, _6, N, ...) N #define _Num_ARGS_(... ) __VA_ARG_N(__VA_ARGS__) #define NUM_ARGS(...) (_Num_ARGS_(_0, ## __VA_ARGS__, Max_ITEMS()) - 1) #define CHECK_ARGS_MAX_LIMIT(t) if(NUM_ARGS(args)>t) #define CHECK_ARGS_MIN_LIMIT(t) if(NUM_ARGS(args) #define print(x , args ...) \ CHECK_ARGS_MIN_LIMIT(1) printf("error");fflush(stdout); \ CHECK_ARGS_MAX_LIMIT(4) printf("error");fflush(stdout) ; \ (( \ if (__builtin_types_compatible_p (typeof (x), int)) \ printA(x, ##args); \ else \ printB (x,##args); \ )) int main(int argc, char* * argv) ( int a=0; print(a); print("hello"); return (EXIT_SUCCESS); )

    It will output 0 and hello from printA and printB.

    If your compiler is gcc and you don't mind doing manual updates every time you add a new overload, you can make a macro mass and get the result you want from the callers point of view, not so nice to write... but it's possible

    look at __builtin_types_compatible_p then use it to define a macro that does something like

    #define foo(a) \ ((__builtin_types_compatible_p(int, a)?foo(a):(__builtin_types_compatible_p(float, a)?foo(a):)

    but yeah, nasty, just not

    EDIT: C1X will get support for type expressions, which they look like this:

    #define cbrt(X) _Generic((X), long double: cbrtl, \ default: cbrt, \ float: cbrtf)(X)

    As already stated, overloading in the sense that you mean is not supported by C. The usual idiom to solve the problem is for the function to take a tagged union . This is implemented using the struct parameter, where the struct itself consists of some type of type indicator, such as an enum , and a union of different value types. Example:

    #include typedef enum ( T_INT, T_FLOAT, T_CHAR, ) my_type; typedef struct ( my_type type; union ( int a; float b; char c; ) my_union; ) my_struct; void set_overload (my_struct *whatever) ( switch (whatever->type) ( case T_INT: whatever->my_union.a = 1; break; case T_FLOAT: whatever->my_union.b = 2.0; break; case T_CHAR: whatever-> my_union.c = "3"; ) ) void printf_overload (my_struct *whatever) ( switch (whatever->type) ( case T_INT: printf("%d\n", whatever->my_union.a); break; case T_FLOAT : printf("%f\n", whatever->my_union.b); break; case T_CHAR: printf("%c\n", whatever->my_union.c); break; ) ) int main (int argc, char* argv) ( my_struct s; s.type=T_INT; set_overload(&s); printf_overload(&s); s.type=T_FLOAT; set_overload(&s); printf_overload(&s); s.type=T_CHAR; set_overload(&s) ; printf_overload(&s); )

    Can't you just use C++ and not use all other C++ features besides this one?

    If there hasn't been strictly strict C so far, I would recommend variadic functions instead.

    The following approach is similar to a2800276, but with some C99 macros:

    // we need `size_t` #include // argument types to accept enum sum_arg_types ( SUM_LONG, SUM_ULONG, SUM_DOUBLE ); // a structure to hold an argument struct sum_arg ( enum sum_arg_types type; union ( long as_long; unsigned long as_ulong; double as_double; ) value; ); // determine an array"s size #define count(ARRAY) ((sizeof (ARRAY))/(sizeof *(ARRAY))) // this is how our function will be called #define sum(...) _sum( count(sum_args(__VA_ARGS__)), sum_args(__VA_ARGS__)) // create an array of `struct sum_arg` #define sum_args(...) ((struct sum_arg )( __VA_ARGS__ )) // create initializers for the arguments #define sum_long (VALUE) ( SUM_LONG, ( .as_long = (VALUE) ) ) #define sum_ulong(VALUE) ( SUM_ULONG, ( .as_ulong = (VALUE) ) ) #define sum_double(VALUE) ( SUM_DOUBLE, ( .as_double = (VALUE) ) ) // our polymorphic function long double _sum(size_t count, struct sum_arg * args) ( long double value = 0; for(size_t i = 0; i< count; ++i) { switch(args[i].type) { case SUM_LONG: value += args[i].value.as_long; break; case SUM_ULONG: value += args[i].value.as_ulong; break; case SUM_DOUBLE: value += args[i].value.as_double; break; } } return value; } // let"s see if it works #include int main() ( unsigned long foo = -1; long double value = sum(sum_long(42), sum_ulong(foo), sum_double(1e10)); printf("%Le\n", value); return 0; )

    For the time being, _Generic since the question's _Generic, standard C (no extensions) is effectively received support for overloading functions (rather than operators) thanks to the addition of the _Generic word _Generic in C11. (supported in GCC since version 4.9)

    (Overloading isn't truly "built in" in the way shown in the question, but it's easy to destroy something that works like this.)

    Generic is a compile-time operator in the same family as sizeof and _Alignof . It is described in the standard section 6.5.1.1. It takes two main parameters: an expression (which won't be evaluated at runtime) and a list of type/expression associations, which is a bit like a switch block. _Generic gets the generic type of the expression and then "switches" to it to select the final result expression in the list for its type:

    Generic(1, float: 2.0, char *: "2", int: 2, default: get_two_object());

    The above expression evaluates to 2 - the type of the control expression is int , so it selects the expression associated with int as the value. None of this is left at runtime. (The default clause is mandatory: if you don't specify it and the type doesn't match, it will cause a compilation error.)

    A technique that is useful for function overloading is that it can be inserted by the C preprocessor and select a result expression based on the type of the arguments passed to the controlling macro. So (example from the C standard):

    #define cbrt(X) _Generic((X), \ long double: cbrtl, \ default: cbrt, \ float: cbrtf \)(X)

    This macro implements the overloaded cbrt operation by passing on the type of the argument to the macro, selecting the appropriate implementation function, and then passing the original macro to that function.

    So, to implement your original example, we could do this:

    Foo_int (int a) foo_char (char b) foo_float_int (float c , int d) #define foo(_1, ...) _Generic((_1), \ int: foo_int, \ char: foo_char, \ float: _Generic(( FIRST(__VA_ARGS__,)), \int: foo_float_int))(_1, __VA_ARGS__) #define FIRST(A, ...) A

    In this case, we could use the default: binding for the third case, but that doesn't demonstrate how to extend the principle to multiple arguments. The end result is that you can use foo(...) in your code without worrying (much) about the type of your arguments.

    For more complex situations, such as functions that overload more arguments or change numbers, you can use utility macros to automatically generate static dispatch structures:

    void print_ii(int a, int b) ( printf("int, int\n"); ) void print_di(double a, int b) ( printf("double, int\n"); ) void print_iii(int a, int b, int c) ( printf("int, int, int\n"); ) void print_default(void) ( printf("unknown arguments\n"); ) #define print(...) OVERLOAD(print, (__VA_ARGS__), \ (print_ii, (int, int)), \ (print_di, (double, int)), \ (print_iii, (int, int, int)) \) #define OVERLOAD_ARG_TYPES (int, double) #define OVERLOAD_FUNCTIONS (print) #include "activate-overloads.h" int main(void) ( print(44, 47); // prints "int, int" print(4.4, 47); // prints "double, int" print (1, 2, 3); // prints "int, int, int" print(""); // prints "unknown arguments" )

    (implementation here). With some effort, you can reduce the boilerplate to look pretty similar to a language with built-in overload support.

    Aside, it was already possible to overload amount arguments (rather than type) in C99.

    Note that the way C is evaluated can move you. This will select foo_int if you try to pass a literal character to it, for example, and you need some foo_int if you want your overloads to support string literals. However, overall pretty cool.

    Leushenko's answer is really cool: only the foo example doesn't compile with GCC, which fails on foo(7) , bumping into the FIRST macro and the actual function call ((_1, __VA_ARGS__) , remaining with an extra comma. Also, we run into problems, if we want to provide additional overloads like foo(double) .

    So I decided to answer this question in more detail, including allowing the empty overload (foo(void) - which caused some trouble...).

    The idea now is: define more than one generic in different macros and let the correct one be chosen according to the number of arguments!

    The number of arguments is pretty simple, based on this answer:

    #define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) #define CONCAT(X, Y) CONCAT_(X, Y) # define CONCAT_(X, Y) X ## Y

    That's good, we're deciding either SELECT_1 or SELECT_2 (or more arguments if you want/need them), so we just need the appropriate definitions:

    #define SELECT_0() foo_void #define SELECT_1(_1) _Generic ((_1), \ int: foo_int, \ char: foo_char, \ double: foo_double \) #define SELECT_2(_1, _2) _Generic((_1), \ double : _Generic((_2), \ int: foo_double_int \) \)

    First, an empty macro call (foo()) still creates a token, but it's empty. So the count macro actually returns 1 instead of 0, even if the macro is called empty. We can "easily" fix this problem if we __VA_ARGS__ with a comma after __VA_ARGS__ conditionally, depending on whether the list is empty or not:

    #define NARG(...) ARG4_(__VA_ARGS__ COMMA(__VA_ARGS__) 4, 3, 2, 1, 0)

    it looked easy, but the COMMA macro is quite heavy; fortunately, this topic is already covered in Jens Gustedt's blog (thanks, Jens). The main trick is that function macros don't expand unless followed by parentheses, see Jens blog for further explanation... We just need to modify the macros a bit for our needs (I'll use shorter names and fewer arguments for brevity).

    #define ARGN(...) ARGN_(__VA_ARGS__) #define ARGN_(_0, _1, _2, _3, N, ...) N #define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 1, 0 ) #define SET_COMMA(...) , #define COMMA(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_COMMA(SET_COMMA __VA_ARGS__), \ HAS_COMMA(SET_COMMA __VA_ARGS__ ()) \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _2 ## _3 # define COMMA_0000 , #define COMMA_0001 #define COMMA_0010 , // ... (all others with comma) #define COMMA_1111 ,

    And now we're fine...

    Full code in one block:

    /* * demo.c * * Created on: 2017-09-14 * Author: sboehler */ #include void foo_void(void) ( puts("void"); ) void foo_int(int c) ( printf("int: %d\n", c); ) void foo_char(char c) ( printf("char: %c \n", c); ) void foo_double(double c) ( printf("double: %.2f\n", c); ) void foo_double_int(double c, int d) ( printf("double: %.2f, int: %d\n", c, d); ) #define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) # define CONCAT(X, Y) CONCAT_(X, Y) #define CONCAT_(X, Y) X ## Y #define SELECT_0() foo_void #define SELECT_1(_1) _Generic ((_1), \ int: foo_int, \ char : foo_char, \ double: foo_double \) #define SELECT_2(_1, _2) _Generic((_1), \ double: _Generic((_2), \ int: foo_double_int \) \) #define ARGN(...) ARGN_( __VA_ARGS__) #define ARGN_(_0, _1, _2, N, ...) N #define NARG(...) ARGN(__VA_ARGS__ COMMA(__VA_ARGS__) 3, 2, 1, 0) #define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 0) #define SET_COMMA(...) , #define COMMA(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_C OMMA(SET_COMMA __VA_ARGS__), \ HAS_COMMA(SET_COMMA __VA_ARGS__ ()) \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _2 ## _3 COMMA_1001 , #define COMMA_1010 , #define COMMA_1011 , #define COMMA_1100 , #define COMMA_1101 , #define COMMA_1110 , #define COMMA_1111 , int main(int argc, char** argv) ( foo(); foo(7); foo(10.12); foo(12.10, 7); foo((char)"s"); return 0; )

    The C++ language implements the ability to use the same identifier for functions that perform different actions on different data types, as a result of which you can use several functions with the same name, but with different parameter lists, both in number and in type.

    Such functions are called overloaded, and the mechanism itself overloadfunctions.

    The compiler determines which of the functions with the same name should be called by comparing the types of the actual arguments with the types of the formal parameters in the headers of all these functions, i.e. the compiler, depending on the type and number of arguments, will form the necessary call to the corresponding function.

    Finding the function to call is done in three separate steps:

    1. Search for a function with an exact match of the parameters and use it if it is found.

    2. Search for the appropriate function using built-in data type conversions.

    3. Search for the appropriate function using user-defined transformations.

    Function overloading example

    Let's give an example of the function S 1 with two parameters X,at, which works depending on the type of arguments passed, as follows:

    – if the parameter type is integer, the function S 1 adds their values ​​and returns the resulting sum;

    – if the parameter type long, function S 1 multiplies their values ​​and returns the resulting product;

    – if the parameter type is real, the function S 1 divides their values ​​and returns the quotient.

    #include

    int S1 (int x, int y) (

    long S1 (long x, long y) (

    double S1 (double x, double y) (

    int a = 1, b = 2, c;

    long i = 3, j = 4, k;

    double x = 10, y = 2, z;

    printf("\n c = %d; k = %ld; z = %lf . \n", c, k, z);

    As a result, we get:

    c = 3; k = 12; z = 5.000000 .

    Variable Parameter Functions

    An ellipsis in the parameter list of a user-defined function is used when the number of arguments is not known in advance. In this case, an indefinite number of parameters can be specified in its prototype as follows:

    void f1 (int a , double b , …);

    Such a notation tells the compiler that behind the required actual arguments for parameters a and b may or may not follow other arguments when calling this function.

    We list the main features of using this mechanism.

    1. Several macro commands are used to access the parameters of such functions, these are:

    va _ list and va _ start – macros for preparing access to parameters;

    va _ arg – use of parameters;

    va _ end - pre-exit cleaning.

    They are declared in the header file stdarg . h .

    2. Such a function must have at least one (named) parameter to pass the number of arguments to be passed to it.

    3. For macro va_ start you must pass two arguments - the name of the parameter list, which specifies va_ list and their number.

    4. It is impossible to break the specified order of macros. Otherwise, you can get unpredictable consequences.

    5. For macro va_ arg besides the name of the list of parameters, you also need to pass the intended type. If the types do not match, an error occurs.

    The use of ellipsis completely disables parameter type checking. The ellipsis is needed only if both the number of parameters and their type are changed.

    The following example illustrates this possibility.

    #include

    #include

    void f1(double s, int n ...) (

    va_start(p, n);

    printf(" \n Double S = %lf ", s);

    for(int i=1; i<=n; i++) {

    v = va_arg(p, int);

    printf("\n Argument %d = %d ", i, v);

    void main(void) (

    f1(1.5, 3, 4, 5, 6);

    As a result, we get:

    Double S = 1.500000

    Argument 1 = 4

    Argument 2 = 5

    Argument 3 = 6

    Press any key to continue