Topic 8: C++ Functions
Synopsis
Overloading functions
- 
In C, every function must have a unique name
  
- 
In C++, two or more functions can have the same name...
  
- 
...provided they have different parameter lists:
 
int   max(int a, int b)     { return (a>b) ? a : b; }
float max(float a, float b) { return (a>b) ? a : b; }
char* max(char* a, char* b) { return (strcmp(a,b)>0) ? a : b; }
void print(char c)      { putchar(c); }
void print(char* cp)    { printf("%s",cp); }
void print(int i)       { printf("%d",i); }
void print(float f)     { printf("%f",f); }
void print(Student s)   { printf("{%d, %s, %f}",
                                 s->idNum, s->name, s->mark); }
Overloaded member¤
functions
- 
Member¤
functions of a class¤
can also be overloaded:
 
class Date
{
public:
        Date(void);
        bool SetDate(char* dateStr);
        bool SetDate(int year, int month, int day);
        bool SetDate(double starDate);
};
int main(void)
{
        Date d;
        d.SetDate("5 October 1964");
        d.SetDate(1998,7,13);
        d.SetDate(1305.4);
}
Dispatching an overloaded
function¤
- 
Which overloaded function¤
gets called depends on the types and number of arguments that are passed:
 
double d = max(1.1,2.2);        // max(float,float)
print(max(1,2));                // max(int,int) -> print(int)
print(max("angel","devil")[d]); // max(char*,char*) -> print(char)
It doesn't depend on the return type:
int i = max(1.1,2.2);           // max(float,float) CALLED
If there isn't an exact match, the "best" nearest match is selected:
max(1,'a');                     // max(int,int) AFTER 'a' PROMOTED
The "best" match is defined by a complicated set of rules which generally
boil down to: "find the unique smallest set of argument conversions which
makes the actual arguments match one of the set of potential parameters"
 
Hence max(1,'a') invokes max(int,int) because
only one conversion ('a' -> (int)'a') is
required.
 
By comparison, to call max(float,float) would require 1
-> 1.0 and 'a' -> (float)'a'.
Likewise calling max(char*,char*) requires two casts: 1
-> (char*)1 and 'a'->(char*)'a'.
 
Note that in the case of max(char*,char*) the function
is still considered, even though the casts will produce invalid char*
values.
 
The dispatch¤
resolution algorithm (simplified)
- 
When the compiler encounters a function call it uses the following algorithm
to determine which function to actually dispatch¤:
  
| 1. | If the function name is not overloaded, dispatch that one. | 
| 2. | Otherwise... 
| 2.1. | Form a set of candidate functions (basically any visible function with the right name) |  
| 2.2. | For a subset of viable functions (candidate functions with the right number of arguments) |  
| 2.3. | For each viable function F... 
| 2.3.1. | Determine the number of argument conversions AC(F) required to cause the argument list to match the types in F's parameter list exactly |  
 
 |  
| 2.4. | If there is a single viable function B such that AC(B) is less than AC(F) for all F except B, then dispatch B |  
| 2.5. | Otherwise flag an ambiguity or an error |  
 
 | 
Resolution failure
- 
Resolution can fail if there is no viable function:
 
int   max(int,int);
float max(float,float);
int main(void)
{
        StudRec s,r;
        max(s,r);       // OVERLOADING RESOLUTION ERROR...
                        // max(StudRec,StudRec) NOT DEFINED
                        // AND NO CONVERSION SEQUENCE AVAILABLE
        max(1,2,3);     // OVERLOADING RESOLUTION ERROR
                        // NO VIABLE max FUNCTION OF 3 ARGS
}
...or if there are two or more equally-good viable functions:
int   max(int,int);
float max(float,float);
int main(void)
{
        max(1,2.2);     // OVERLOADING RESOLUTION ERROR...
                        // CONVERT 1->1.0 OR 2.2->2 ????
}
...or if the best viable function is inaccessible:
class File
{
public:
        // CTORS, DTORS, ETC. AND THEN:
        void Append(char* text);
private:
        void Append(char c);    // A UTILITY FUNCTION PRESUMABLY
                                // USED BY File::Append(char*)
};
int main(void)
{
        File logfile;
        logfile.Append('a');    // OVERLOADING RESOLUTION ERROR...
                                // File::Append(char) IS BEST 
                                // VIABLE, BUT IS INACCESSIBLE
}
const functions
- 
Member¤
functions are often called either to retrieve data from an object or to
store data in that object:
 
class Coefficient
{
public:
        Coefficient(double initValue)
        { myValue = initValue; }
        double GetValue(void)
        { return myValue; }
        bool SetValue(double v)
        {
                if (v<0 || v>1) { return false; }
                myValue = v;
                return true;
        }
private:
        double myValue;
};
Those that just retrieve data are "safe" to call on any valid object:
Coefficient neutronRate(0.33);
double currVal;
// AND LATER...
currVal = neutronRate.GetValue();
But functions which set data can be a problem if the object is declared
const:
const Coefficient criticalNeutronRate(0.5);
// AND LATER...
criticalNeutronRate.SetValue(0.99) // DANGER, WILL ROBINSON!
To avoid this kind of problem, C++ enforces the rule that you cannot call
ordinary member¤
functions on a const object.
 
But that means that we have no way of getting the critical neutron
count!
 
So C++ provides a means for us to specify whether a function is safe to
call on a const object:
class Coefficient
{
public:
        Coefficient(double initValue)
        { myValue = initValue; }
        double GetValue(void) const
        { return myValue; }
        bool SetValue(double v)
        { if (v<0 || v>1) { return false; }
                myValue = v;
                return true;
        }
private:
        double myValue;
};
Any member¤
function declared with the keyword const after its parameter
list may be called on a const object.
 
Any member¤
function declared without the keyword const after
its parameter list may not be called on a const
object.
 
Const member¤
functions may only call other const member¤
functions of the same object, and may not modify the value of any data
member¤¤
of the object...
 
...except if that data member¤¤
is declared mutable:
class Coefficient
{
public:
        Coefficient(double initValue)
        { myValue = initValue; myAccesses = 0; }
        double GetValue(void) const
        { myAccesses++; return myValue; }
        bool SetValue(double v)
        {
                myAccesses++;
                if (v<0 || v>1) { return false; }
                myValue = v; return true;
        }
private:
        double myValue;
        mutable int myAccesses;
};
Copy constructors¤¤
- 
Constructors¤
give us a way of initializing an object with one (or more) values.
  
- 
But consider how we would define a constructor¤
which initializes an object with the value(s) of another object of the
same type. For example:
 
Date d1 (1998,4,1); // INIT TO 1 APRIL
Date d2 (d1);       // INIT TO SAME DATE AS d1
Such constructors¤
are called "copy constructors¤¤"
 
There's a problem if we write the constructor¤
in the "obvious" way:
class Date
{
public:
        Date(int year,int month,int day)
        { myYear = year; myMonth = month, myDay = day; }
        Date(Date d)
        { myYear = d.myYear; myMonth = d.myMonth; myDay = d.myDay; }
private:
        int myYear;
        int myMonth;
        int myDay;
};
Date d1 (1998,4,1); // INIT TO 1 APRIL
Date d2 (d1);       // INIT TO SAME DATE AS d1
The problem
- 
In order to initialize d2 we have to call the Date(Date
d) constructor¤.
  
- 
In order to do that we need to make a temporary object (the parameter d)
of type Date, with the same value as d1.
  
- 
When we create the temporary d, its constructor¤
gets called and is passed d1.
  
- 
But now we're initializing a Date object (d)
with another Date object (d1). So the copy
constructor¤¤
(Date(Date d)) gets called!
  
- 
In order to do that we need to make another temporary parameter (call this
one d') of type Date, with the same value
as d.
  
- 
When we create the temporary d', its constructor¤
gets called and is passed d.
  
- 
But now we're initializing a Date object (d')
with another Date object (d). So the copy
constructor¤¤
(Date(Date d)) gets called!
  
- 
In order to do that we need to make another temporary parameter (call this
one d'') of type Date, with the same value
as d'.
  
- 
When we create the temporary d'', its constructor¤
gets called and is passed d'.
  
- 
But now we're initializing a Date object (d'')
with another Date object (d'). So the copy
constructor¤¤
(Date(Date d)) gets called!
  
- 
Et cetera, et cetera, et cetera....
  
 
The solution
- 
We must avoid copying the single Date parameter
when we invoke the copy constructor¤¤
  
- 
We can do that by passing the parameter as a reference¤:
 
class Date
{
public:
        Date(int year,int month,int day); // AS BEFORE
        Date(Date& d)
        { myYear = d.myYear; myMonth = d.myMonth; myDay = d.myDay; }
        // ETC...
};
Date d1 (1998,4,1); // INIT TO 1 APRIL
Date d2 (d1);       // INIT TO SAME DATE AS d1
Now when we initialize d2, we actually pass a reference¤
to the original d1 into Date(Date& d),
instead of a copy.
 
Since no copy is required, there are no recursive calls to the copy constructor¤¤.
 
Finally, it's almost always better to define the copy constructor¤¤
like this:
class Date
{
public:
        Date(const Date& d)
        { myYear = d.myYear; myMonth = d.myMonth; myDay = d.myDay; }
        // ETC...
};
Why?
 
Reading
- Lippman & Lajoie
 
- New: Chapter 14.
 
 
- Stroustrup
 
    
      - New: Chapter 10.
 
      
      
        
    
  
  This material is part of the CSE2305 - Object-Oriented 
  Software Engineering course. 
  Copyright © Jon McCormack & Damian Conway, 1998–2005. All rights 
  reserved.