1.5. Introduction to Functions

[ fromfile: cppintro.xml id: intro-functions ]

Every modern programming language has a way for programmers to define functions. Functions enable you to divide a program into manageable components, instead of presenting it as a large, complex monolith. This enables you to develop and test small components individually, or in small groups, and makes it easier to build and maintain software. Functions also provide a way to write reusable code for specific tasks. For example, Example 1.1 computes the factorial of a given number inside the main() function. Example 1.3 shows how to extract the code for computing factorials and transform it into a reusable function.

Example 1.3. src/early-examples/fac2.cpp


#include <iostream> 

long factorial(long n) {
    long ans = 1;
    for (long i = 2; i <= n; ++i) {
        ans = ans * i;
        if (ans < 0) {
            return -1;
        }
    }
    return ans;
}

int main() {
    using namespace std;
    cout << "Please enter n: " << flush;
    long n;        1
    cin >> n;      2 

    if (n >= 0) { 
        long nfact = factorial(n);
        if (nfact < 0) {
            cerr << "overflow error: " 
                 << n << " is too big." << endl;
        }
        else {
            cout << "factorial(" << n << ") = "
                 << nfact << endl;
        }
    }
    else {
        cerr << "Undefined:  "
             << "factorial of a negative number: " << n << endl;
    }
    return 0;
}

1

long int

2

read from stdin, try to convert to long


With the exception of constructors and destructors, [9] discussed in Chapter 2, and conversion operators, discussed in Section 19.9.1, every function must have

  1. A return type (which may be void)

  2. A name

  3. An ordered, comma-separated list (which may be empty) of the types of the function's parameters

  4. A body (a block of zero or more statements enclosed in {braces})

The first three are the function's interface, and the last is its implementation.

In Example 1.3 the function definition appears above the statement that invokes it; however, it may not always be possible or desirable to place a function definition before every instance in which it is called. C and C++ allow a function to be called before it has been defined, as long as the function has been declared prior to the call.

The mechanism for declaring a function (i.e., describing to the compiler how it is to be invoked) is the function prototype. A function prototype includes the following information:

In other words, everything except the function's body. Here are a few prototypes.

int toCelsius(int fahrenheitValue);
QString toString();
double grossPay(double hourlyWage, double hoursWorked);

Remember, a function must be declared or defined before it is used for the first time so that the compiler can set up calls to it properly. We discuss declarations and definitions in more detail in Section 20.1. It is an error to omit the return type (even if it is void) except for the main() function, in which case the return type implicitly defaults to int.

Although parameter names are optional in function prototypes, it is good programming practice to use them. They constitute an effective and efficient part of the documentation for a program.

A simple example can help to show why parameter names should be used in function prototypes. Suppose you needed a function to set a Date with values for the year, month, and day. If you presented the prototype as setDate(int, int, int), the programmer working with Dates would not know immediately from the prototype alone in what order to list the three values when calling that function. Because at least three of the possible orderings are in common use somewhere on the planet, there is no "obvious" answer that would eliminate the need for more information. As you can see in Section 2.2, the definition of a member function is usually kept in a separate file from the declaration (and might not be accessible), so a programmer might have some difficulty figuring out how to call the function. By giving the parameters good names, that problem is eliminated and the function has, at least partially, documented itself. [10]

Function Overloading

C++ permits overloading of function names. This enables programmers to attach the same function name to different implementations with different parameters.

The signature of a function consists of its name and its parameter list. In C++, the return type is not part of the signature.

A function name is overloaded if two or more functions within a given scope have the same name but different signatures. It is an error to have two functions in the same scope with the same signature but different return types. Overloading requires the compiler to determine, by analyzing the argument list, which version of an overloaded function gets executed in response to a function call. Because the decision is entirely based upon the argument list, it is easy to see why the compiler cannot permit functions with the same signature but different return types to coexist in the same scope. We discuss that decision process in Section 5.1. In the meantime, Example 1.4 provides some function calls to ponder.

Example 1.4. src/functions/overload-not.cpp

#include <iostream>
using namespace std;

void foo(int n) {
  cout << n << " is a nice number." << endl;
}


int main() {
   cout << "before call: " << 5 << endl;
   foo(5);
   cout << "before call: " << 6.7 << endl;
   foo(6.7);
   cout << "before call: " << true << endl;
   foo(true);
}

Here there is only one function but we call it with three different numerical types. In this case, automatic type conversions permit the function to be called three times.

  src/functions> g++ overload-not.cpp
  src/functions> ./a.out
  before call: 5
  5 is a nice number.
  before call: 6.7
  6 is a nice number.
  before call: 1
  1 is a nice number.
  src/functions>
  

This output shows some of the harsh realities of numerical types. First, when a floating point number gets converted to an int, its fractional part (the decimal point and all digits to the right of it) is discarded. Even though 6.7 is closer to 7 than to 6, no rounding takes place. Second, the bool value true is displayed as 1 (and false is displayed as 0). If you want to see the word true (or false) you need to add code to output the appropriate strings as shown in Example 1.5.

Now, let's use overloaded functions.

Example 1.5. src/functions/overload.cpp

#include <iostream>
using namespace std;

void foo(int n) {
  cout << n << " is a nice int." << endl;
}

void foo(double x) {
  cout << x << " is a nice double." << endl;
}

void foo(bool b) {
   cout << "Always be " << (b?"true":"false") << " to your bool." << endl;
}

int main() {
  foo(5);
  foo(6.7);
  foo(true);
}

With three overloaded versions of the function, no type conversions are necessary when using the same main() function. Notice the use of the conditional operator in the third version of foo().[11]

  src/functions> g++ overload.cpp
  src/functions> ./a.out
  5 is a nice int.
  6.7 is a nice double.
  Always be true to your bool.
  src/functions>
  

Chapter 5 discusses in more detail the many interesting and useful features of C++ functions.



[9] A constructor must not have a return type and may have an empty body. A destructor must not have a return type, must have an empty parameter list, and may have an empty body.

[10] In Section 15.2 we discuss an example in which it can be advantageous to omit some parameter names.

[11] The ternary conditional operator, testExpr ? valueIfTrue : valueIfFalse provides a terse way to insert a simple choice into an expression. If testExpr has a nonzero value (e.g., true) the value immediately to the right of the question mark (?) is returned. If testExpr has value 0 (e.g., false) the value to the right of the colon (:) is returned.