Modern C++ Basics Part -5

Functions are important building blocks of C++ programs.

1.5 Functions

Functions are important building blocks of C++ programs. The first example we have seen is the main function in the hello-world program. We will say a little more about the main in Section 1.5.5.

The general form of a C++ function is

[inline] return_type function_name (argument_list)
{
    body of the function
}

In this section, we discuss these components in more detail.

1.5.1 Arguments

C++ distinguishes two forms of passing arguments: by value and by reference.

1.5.1.1 Call by Value

When we pass an argument to a function, it creates a copy by default. For instance, the following function increments x but not visibly to the outside world:

void increment(int x)
{
    x++;
}

int main()
{
    int i= 4;
    increment(i);       // Does not increment i
    cout ≪ "i is " ≪ i ≪ '\n';
}

The output is 4. The operation x++ within the increment function only increments a local copy of i but not i itself. This kind of argument transfer is referred to as Call-by-Value or Pass-by-Value.

1.5.1.2 Call by Reference

To modify function parameters, we have to Pass the argument by Reference:

void increment(int& x)
{
    x++;
}

Now, the variable itself is incremented and the output will be 5 as expected. We will discuss references in more detail in §1.8.4.

Temporary variables—like the result of an operation—cannot be passed by reference:

increment(i + 9); // Error: temporary not referable

since we could not compute (i + 9)++ anyway. In order to call such a function with some temporary value, we need to store it first in a variable and pass this variable to the function.

Larger data structures like vectors and matrices are almost always passed by reference to avoid expensive copy operations:

double two_norm(vector& v) { ... }

An operation like a norm should not change its argument. But passing the vector by reference bears the risk of accidentally overwriting it. To make sure that our vector is not changed (and not copied either), we pass it as a constant reference:

double two_norm(const vector& v) { ... }

If we tried to change v in this function the compiler would emit an error.

Both call-by-value and constant references ascertain that the argument is not altered but by different means:

  • Arguments that are passed by value can be changed in the function since the function works with a copy.6
  • With const references we work directly on the passed argument, but all operations that might change the argument are forbidden. In particular, const-reference arguments cannot appear on the left-hand side (LHS) of an assignment or be passed as non-const references to other functions (in fact, the LHS of an assignment is also a non-const reference).

In contrast to mutable7 references, constant ones allow for passing temporaries:

alpha= two_norm(v + w);

This is admittedly not entirely consequential on the language design side, but it makes the life of programmers much easier.

1.5.1.3 Defaults

If an argument usually has the same value, we can declare it with a default value. Say we implement a function that computes the n-th root and mostly the square root, then we can write

double root (double x, int degree= 2) { ... }

This function can be called with one or two arguments:

x= root(3.5, 3);
y= root(7.0);     // like root (7.0, 2)

We can declare multiple defaults but only at the end of the argument list. In other words, after an argument with a default value we cannot have one without.

Default values are also helpful when extra parameters are added. Let us assume that we have a function that draws circles:

draw_circle(int x, int y, float radius);

These circles are all black. Later, we add a color:

draw_circle(int x, int y, float radius, color c= black);

Thanks to the default argument, we do not need to refactor our application since the calls of draw_circle with three arguments still work.

1.5.2 Returning Results

In the examples before, we only returned double or int. These are well-behaved return types. Now we will look at the extremes: large or no data.

1.5.2.1 Returning Large Amounts of Data

Functions that compute new values of large data structures are more difficult. For the details, we will put you off till later and only mention the options here. The good news is that compilers are smart enough to elide the copy of the return value in many cases; see Section 2.3.5.3. In addition, the move semantics (Section 2.3.5) where data of temporaries is stolen avoids copies when the before-mentioned elision does not apply. Advanced libraries avoid returning large data structures altogether with a technique called expression templates and delay the computation until it is known where to store the result (Section 5.3.2). In any case, we must not return references to local function variables (Section 1.8.6).

1.5.2.2 Returning Nothing

Syntactically, each function must return something even if there is nothing to return. This dilemma is solved by the void type named void. For instance, a function that just prints x does not need to return something:

void print_x(int x)
{
    std::cout ≪ "The value x is " ≪ x ≪ '\n';
}

void is not a real type but more of a placeholder that enables us to omit returning a value. We cannot define void objects:

void nothing;      // Error: no void objects

A void function can be terminated earlier:

void heavy_compute(const vector& x, double eps, vector& y)
{
    for (...) {
        ...
        if (two_norm(y) < eps)
            return;
    }
}

with a no-argument return.

1.5.3 Inlining

Calling a function is relatively expensive: registers must be stored, arguments copied on the stack, and so on. To avoid this overhead, the compiler can inline function calls. In this case, the function call is substituted with the operations contained in the function. The programmer can ask the compiler to do so with the appropriate keyword:

inline double square(double x) { return x*x; }

However, the compiler is not obliged to inline. Conversely, it can inline functions without the keyword if this seems promising for performance. The inline declaration still has its use: for including a function in multiple compile units, which we will discuss in Section 7.2.3.2.

1.5.4 Overloading

In C++, functions can share the same name as long as their parameter declarations are sufficiently different. This is called Function Overloading. Let us first look at an example:

#include <iostream>
#include <cmath>

int divide (int a, int b) {
    return a / b ;
}

float divide (float a, float b) {
    return std::floor( a / b ) ;
}

int main() {
    int   x= 5, y= 2;
    float n= 5.0, m= 2.0;
    std::cout ≪ divide(x, y) ≪ std::endl;
    std::cout ≪ divide(n, m) ≪ std::endl;
    std::cout ≪ divide(x, m) ≪ std::endl; // Error: ambiguous
}

Here we defined the function divide twice: with int and double parameters. When we call divide, the compiler performs an Overload Resolution:

  1. Is there an overload that matches the argument type(s) exactly? Take it; otherwise:
  2. Are there overloads that match after conversion? How many?
    • 0: Error: No matching function found.
    • 1: Take it.
    • > 1: Error: ambiguous call.

How does this apply to our example? The calls divide(x, y) and divide(n, m) are exact matches. For divide(x, m), no overload matches exactly and both by Implicit Conversion so that it’s ambiguous.

The term “implicit conversion” requires some explanation. We have already seen that the numeric types can be converted one to another. These are implicit conversions as demonstrated in the example. When we later define our own types, we can implement a conversion from another type to it or conversely from our new type to an existing one. These conversions can be declared explicit and are then only applied when a conversion is explicitly requested but not for matching function arguments.

⇒ c++11/overload_testing.cpp

More formally phrased, function overloads must differ in their Signature. The signature consists in C++ of

  • The function name;
  • The number of arguments, called Arity; and
  • The types of the arguments (in their respective order).

In contrast, overloads varying only in the return type or the argument names have the same signature and are considered as (forbidden) redefinitions:

void f(int x) {}
void f(int y) {} // Redefinition: only argument name different
long f(int x) {} // Redefinition: only return type different

That functions with different names or arity are distinct goes without saying. The presence of a reference symbol turns the argument type into another argument type (thus, f(int) and f(int&) can coexist). The following three overloads have different signatures:

void f(int x) {}
void f(int& x) {}
void f(const int& x) {}

This code snippet compiles. Problems will arise, however, when we call f:

int       i= 3;
const int ci= 4;

f(3);
f(i);
f(ci);

All three function calls are ambiguous because the best matches are in every case the first overload with the value argument and one of the reference-argument overloads respectively. Mixing overloads of reference and value arguments almost always fails. Thus, when one overload has a reference-qualified argument, then the corresponding argument of the other overloads should be reference-qualified as well. We can achieve this in our toy example by omitting the value-argument overload. Then f(3) and f(ci) will resolve to the overload with the constant reference and f(i) to that with the mutable one.

1.5.5 main Function

The main function is not fundamentally different from any other function. There are two signatures allowed in the standard:

int main()

or

int main(int argc, char* argv[])

The latter is equivalent to

int main(int argc, char** argv)

The parameter argv contains the list of arguments and argc its length. The first argument (argc[0]) is on most systems the name of the called executable (which may be different from the source code name). To play with the arguments, we can write a short program called argc_argv_test:

int main (int argc, char* argv[])
{
    for (int i= 0; i < argc; ++i)
        cout ≪ argv[i] ≪ '\n';
    return 0;
}

Calling this program with the following options

argc_argv_test first second third fourth

yields:

argc_argv_test
first
second
third
fourth

As you can see, each space in the command splits the arguments. The main function returns an integer as exit code which states whether the program finished correctly or not. Returning 0 (or the macro EXIT_SUCCESS from <cstdlib>) represents success and every other value a failure. It is standard-compliant to omit the return statement in the main function. In this case, return 0; is automatically inserted. Some extra details are found in Section A.2.5.

Reference Book by Peter Gottschling

Leave a Comment

Your email address will not be published. Required fields are marked *