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.