Vocabulary

Before starting new material, it is worth taking a moment to stop and make sure that we are clear on some of the basic vocabulary used to talk about functions in programs. This was covered in the previous lesson. Hopefully the following annotated program will clarify things a bit.

Functions that return nothing

Sometimes we're interested in functions that return nothing at all. These functions are called for their side effects. This is an issue for our prototype, however, since we need to specify something for return type. In this case, we use void in place of a return type. Here's an example:

void printgreetings();  // void return type: a function that returns nothing

int main()
{
  printgreetings();

  return 0;
}

void printgreetings()
{
  cout << "G R E E T I N G S !" << endl
       << endl
       << "This is a little demo program." << endl
       << "I hope you enjoy it." << endl;   
}
This function is only used for the side effect of writing something on the screen, not for any value it would return.

Predicates

What are predicates?
Functions which return the type bool are traditionally referred to as predicates.

Imagine that you want to know whether an integer is a prime number or not. We could write a function isprime to do this. The prototype would be as follows:


bool isprime(int n);
Note that specifying a name for the parameter is actually not required in the prototype (only in the function definition), so we could simplify the prototype to be:

bool isprime(int);
We can define the function as follows:

bool isprime(int n)
{
  if (i <= 1)
    return false;

  for (int i=2; i < n; i++)
  {
    if (n % i == 0 ) 
      return false;
  }
  
  return true;
}
With the definition of this predicate, you can use the function as follows:

if( isprime(n) )    // isprime returns true or false
  cout << n << " is a prime number!" << endl;

Really there's no need to bring predicates up as a special subject, since functions that return bools are not any more special than functions that return, say, strings. However, you might not have thought much about the use of having such functions.

Multi-Parameter Functions

Functions get infinitely more interesting when they have more than one argument or parameter. Specifying multiple parameters for a function is just like specifying several single parameters in a comma-separated list.

For example, suppose you wanted to define a function max that looked at two ints and returned the larger of the two. It's prototype would be


int max(int,int);
The definition would look like this:

int max(int a, int b)
{
  if (a < b)
    return b;
  else
    return a;
}
Note that this example shows you that you can return from anywhere within a function (e.g., it returns b in the middle of the function definition, if a is less than b), just like you can return from anywhere within main.

Composing Functions

Let's suppose that I had the function max defined as below, using the conditional (or ternary) operator ?:

int max(int a, int b)
{
  return (b > a) ? b : a;
}
but that my program had three ints, x, y and z, amongst which I need the largest. Were I to write
max(x,y,z)
the compiler would complain ... the only max function it knows about only takes two arguments! However, I could say the following:
max(max(x,y),z)
This is our first example of composition of functions. When the function max gets called, its two argument expressions are evaluated. The first is max(x,y), which evaluates to the larger of the two values, and the second is simply z. So, what we get out of this is the maximum of all three values.

The most important thing I can tell you about composing functions, is that there is really nothing to talk about. Function arguments are given in your code by expressions, right? And those expressions are evaluated before calling the function to produce the argument objects that are passed to the function. So, whether or not the argument expressions themselves contain function calls is immaterial — the system works the same.

Remarks on the arguments

Suppose you have a function

void rep(int,char);
The function function name rep stands for repetition. That is, it prints the char argument to the screen the number of times given by the int.

Order of arguments. It's important to note that the order of arguments matters. For example, If I want to print a # symbol 42 times, I need to be sure to say rep(42,'#'), because the function expects the int object first.

Q: If I said rep('#',42) instead, do you know what'd happen?

A: I'd print *, 35 times!

Why? Because the same kind of implicit type conversions that go on inside expressions go on with function arguments!

Arguments' data type. When implicit conversions aren't possible, the compiler gives you an error message.

Q: If you tried to call rep("#",42), what would happen?

A: The compiler would give you an error saying:

You'll likely see lots of these messages in your life!

Overloading functions

Now, what if I add another prototype and definition for the function rep, but with different types of arguments? For example, what if I have the following program
Prototypes Definitions main output

void rep(char,int);
void rep(int,int);

void rep(char c, int k)
{
  for(int i = 0; i < k; i++)
    cout << c;
  cout << endl;
}
void rep(int n, int k);
{
  for(int i = 0; i < k; i++)
    cout << n;
  cout << endl;
}

int main()
{
  int a = 55, b = 6;
  char c = 'X';

  rep(c,b); //Alternative One

  rep(a,b); //Alternative Two

  return 0;
}
XXXXXX
555555555555
What happens here? Which function gets used? This is called overloading of function names. The idea is, that the name of a function isn't just rep, the name of the function is really rep(int,int) or rep(char,int).

Overloading of a function name occurs when two or more definitions are given for functions with the same name, but different number or types of arguments.

Q: Will the following code compile?


double f(int);
char f(int);
A: Note the return type of a function is not considered in function overloading. Thus, it is illegal to have two functions whose prototype differs only in the return type. The above won't compile.

Example: multiple functions that behave similarly

Using the function overloading, you can implement have two max functions taking a different number of arguments.

int max(int a, int b)
{
  if (a >= b)
    return a;
  else
    return b;
}

int max(int a, int b, int c)
{
  return max(max(a,b),c);
}

int main()
{
  cout << max(2, 3) << endl;
  cout << max(2, 4, 3) << endl;
  return 0;
}

Implicit conversions and overloading sometimes create issues.

When you make a function call with arguments whose types and number do not match any prototype, the compiler will try to use implicit type conversion to match a prototype. This is pretty straightforward (remember the example at the top of the page?) when function name overloading is not used. With oveloading, however, things can get complicated. For example:
Definitions Function Call Discussion

void rep(char c, int k)
{
  for(int i = 0; i < k; i++)
    cout << c;
}

void rep(int n, int k);
{
  for(int i = 0; i < k; i++)
    cout << n;
}
rep(42.23,10);
Error! This function call is ambiguous! The compiler has no way of knowing:
  • whether the double 42.23 is supposed to be implicitly converted to the char '*', so we can use void rep(char,int)
  • or whether it should be implicitly converted to the int 42 so we can use void rep(int,int).
This results in a compiler error!
How is the compiler supposed to decide which implicit conversion is best?

Fixing the issues: When ambiguities like this arise, simply use explicit conversion to disambiguate. For the previous example, I might change the call rep(42.23,10); to rep(char(42.23),10);, thus removing any ambiguity.

Practice Problems

  1. Surveying Problem This gives you another simple example of top-down design.
  2. Drawing a triangle with *'s This is a simple multi-parameter function problem.
  3. Distance between points This is another simple multi-parameter function problem.
  4. What percentage of numbers are prime? This is a simple example using a predicate.