When programs get longer and more complicated we often have to reuse the same piece of code. For example, we might have to calculate the average of several different arrays of ints, but creating a variable, iterating over all the elements and summing, then dividing by the number of elements can get quite tedious when done multiple times. To save us from such tedious work we can make use of functions, which are sections of code that can be run multiple times without having to rewrite them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

float calculateAverage(int array[], unsigned int numElements);

int main()
{
    int a[3] = { 4, 5, -9 };
    int b[5] = { 1, 2, 6, -4, 3 };

    std::cout << calculateAverage(a, 3) << std::endl;
    std::cout << calculateAverage(b, 5) << std::endl;

    return 0;
}

float calculateAverage(int array[], unsigned int numElements)
{
    float sum = 0.0f;
    for(int i = 0; i < numElements; ++i)
    {
        sum += array[i];
    }

    return sum / numElements;
}

Here we calculate the average of two different sized arrays by using a function instead of writing the same code twice. A function accepts some variables as arguments, and then uses those arguments to calculate a return value which the function evaluates to. In this case our function takes an array of ints (note that we don’t put the size in the [] because the size varies) and the number of elements in that array as arguments and then returns a float value equal to the average of the elements in the array.

Like variables, functions must be declared before they can be used. Unlike variables though—which can be declared anywhere—functions can only be declared outside of any other code block, in this case on line 3. The return type of the function is placed first and the name of the function comes after. Following that are the arguments of the function separated by , and placed within (), followed by a ; at the end of the line. A function can have no arguments at all, in which case the () are empty but they must be included.

float randf();

Once a function has been declared in can be used even if it doesn’t have any code associated with it. For example, on lines 10 and 11 we pass the two arrays and their sizes to the calculateAverage function, which will then evaluate to their average. This is known as calling the function. Functions can be used in any expression that literals such as 3, 'c', or 2.92f can be, as the function is simply replaced by its return value.

The function still needs to be defined somewhere though, in this case we’ve placed in after main. (Which is also like a function, you can see the similarities with the syntax! You can’t call it like we did with calculateAverage though, and it doesn’t need a declaration.) The first line of the definition looks identical to the declaration but without the ;, which is instead replaced with {} surrounding the body of the function. Everything within the {} is run when the function is called.

Everything else is just standard stuff we’ve seen before, though at the end of the function you must have a return statement which is followed by a value of the same type as the return type of the function. The given value is what the function will evaluate to. return also ends the function, and so placing it in a different place is perfectly valid, but the function will end when it reaches it. For example, a function that returned the index of the first value greater than 3 would use a return statement as soon as that value was found.

int findIndex(int array[], unsigned int numElements)
{
    for(int i = 0; i < numElements; ++i)
    {
        // Return the index of the first element greater than 3
        if(array[i] > 3) return i;
    }
    // This line will only be reached if no such element was found
    // (i.e. the function hasn't returned) and so instead we return
    // a value of -1 to signify an error. (As there is no -1 element)
    return -1;
}

void Functions

Earlier we briefly mentioned the void type and how it’s used as a kind of placeholder in some circumstances. Function return types are one such circumstance, and if you don’t want a function to return a value then you can declare it to be of type void.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

// Function is of type void so it doesn't return anything, but it
// can still have arguments
void printSmallSquare(double b)
{
    if(-100.0f < b && b < 100.0f) std::cout << b*b << std::endl;

    // void functions don't need a return statement, but you can
    // use with one with no value if you want
    // return;
}

int main()
{
    // Attempting to use this in an expression will result in an error
    printSmallSquare(33.0f);

    return 0;
}

As an aside, note that we have omitted the declaration of the printSmallSquare function and instead only included the definition. This is fine but you still have to essentially declare the function before it’s used, so now the definition must be placed before main. It’s the most use when your program is just a single file, but that isn’t always going to be the case, and definitely won’t be for larger projects.

Default Function Arguments

Sometimes functions have a lot of arguments, but some have implied values that don’t normally need to be changed. For example, we might have a function that takes the base 10 logarithm of a number, but can also calculate the logarithm in other bases as well. We can therefore make the base have a default value of 10, but let it take other values too.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

float log(float x, float base = 10.0f)
{
    /* Irrelevant logarithm calculation goes here */
}

int main()
{
    // Equivalent to log(10, 10), which is 1
    std::cout << log(10) << std::endl;
    // Or we can specify the base explicitly
    std::cout << log(8, 2) << std::endl;

    return 0;
}

Default arguments are great, but because the order of the arguments is important, all arguments with default values must be at the end of the function. Additionally, if a function has multiple default arguments then you must give values to all the arguments before the ones you are leaving as default.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

void foo(int a, int b = 10, int c = 4)
{
    std::cout << a + b + c << std::endl;
}

int main()
{
    foo(10); // Will print 24
    // Don't want to change b but must include it because c is changed
    foo(5, 10, 3); // Will print 18.

    return 0;
}

Function Overloading

C++ allows you to overload functions by reusing their name but specifiying different return types and arguments. For example, you could have two log functions, one that used floats and another that used doubles.

float log(float x, float base = 10.0f);
double log(double x, double base = 10.0);

The compiler will automatically work out which version of log to use depending on the type of the arguments passed to it. This means that functions which differ only in their return type cannot be overloaded. An overloaded version of a function is not required to be related to the original, but its bad practice for there to be no link. Don’t write a function to log events and call it the same as a function to calculate logarithms!

References and Functions

We saw earlier that references were a way of creating an additional label for the same variable which allowed you to modify the variable they referred to by just using the reference. They have an excellent application to functions, where we’ll often want to create a function that modifies one or more of its arguments. Simply passing a variable won’t work, because the function only sees the value of the variable; we say it is passed by value. By passing by reference the function can modify one of its arguments and that change will be reflected to the variable passed to it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

void increase(double& x, double y)
{
    x += y;
}

int main()
{
    double value = 3.4;
    increase(value, 10.0);

    std::cout << "Value is " << value << std::endl;

    return 0;
}

However, trying to call increase(3.4, 10.0) will result in a compile error!

references.cpp:3:6: note: candidate function not viable: expects an l-value for 1st argument
void increase(double& x, double y)
     ^
1 error generated.

Now is the time to make the distinction between l-values and r-values. All parts of expressions in C++ are made up of l-values and r-values, essentially with l-values on the left of an expression and r-values on the right. l-values are things that exist beyond a single expression, such as variables, whereas r-values are temporary values that don’t exist after they are used. This includes things such as literals like 3.4 which cannot be modified. This is why expressions like

12.0f = value;

3 * value = 10.0f;

don’t make any sense; they have r-values on the left.

Technically the type& name syntax does not specify a reference but an l-value reference—a reference to an l-value—but 3.4 is an r-value and so compilation fails. It’s possible to define an r-value reference, but we won’t cover them here.

Functions can also use references as return types, which can extend the lifetime (scope) of a variable outside of the function, however this feature is currently useless to us without learning some additional things first. Just remember that it’s possible!

Functions in Other Files

The declaration and definition of a function do not need to be in the same file, and by using include directives you can move entire function definitions out of the file containing main. To do this you must create a header file with the extension .hpp or .h and a corresponding source .cpp file. The header will contain the function declaration, the source file will contain the declaration, and the main source file main.cpp should contain an include directive to include the header file. A code example is simpler!

1
2
3
4
/* power.hpp */

// Compute a raised to the power b
int power(int a, unsigned int b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* power.cpp */

// Include the header file which contains the function's declaration
// Unlike with iostream and cstdlib we have to include the extension
// and use " " instead of < >
#include "power.hpp"

// We don't need the extern here
int power(int a, unsigned int b)
{
    int prod = 1;
    for(unsigned int i = 0; i < b; ++i)
    {
        prod *= a;
    }

    return prod;
}
1
2
3
4
5
6
7
8
9
10
11
/* main.cpp */

#include <iostream>
#include "power.hpp"

int main()
{
    std::cout << "3^10 = " << pow(3, 10) << std::endl;

    return 0;
}

This is a great way of separating and simplifying code, but it can cause problems if a source file includes the same header file twice. This problem isn’t as simple to fix as just not writing #include "power.hpp" twice in one file, as you may be in the sitation where you have two headers—say area.hpp and volume.hpp—which both include the power.hpp header. If you include both of these in one source file, then power.hpp will be included twice—once for each header—and you will get an error. To fix this, we use preprocessor directives. power.hpp would become

1
2
3
4
5
6
7
8
9
10
/* power.hpp */
// If the compiler hasn't encountered POWER_HPP yet
#ifndef POWER_HPP
// Then define POWER_HPP so this code doesn't run again
#define POWER_HPP

int power(int a, unsigned int b);

// Everything up to this will only be run once
#endif

The preprocessor can define certain identifiers, called macros, which are a bit like functions except are run when the program is compiled and not when it is opened. Here we check if a macro name unique to this header has already been defined, and if it has we tell the compiler not to process any of the file. If it isn’t already defined, we define it and the compiler proceeds as normal. This way the header is effectively only included once.

Exercise

A vector is a mathematical object which as well as having a sign also has a direction specified by a component along each axis (x, y, and z, for example). The vector has an x component of 2, a y component of 1, and a z component of 2. The magnitude of a vector is defined to be the length of the line from the origin to the components of the vector, and is equal to the square root of the sum of the squares of its components. The length of is .

Your task is to write a mag function which takes either a vector with an arbitrary number of components or a single float and computes their magnitude. You should also write the function to calculate the square root yourself (no cmath for those of you who’ve investigated the standard library), which can be calculated by starting with an initial guess and then iteratively computing As gets larger tends to the actual square root. (This recurrence relation was derived using the Newton Rhapson procedure.)

Both of these should be declared and defined in a single header and source file.

Solution
1
2
3
4
5
6
7
8
9
10
11
12
13
/* vector.hpp */
#ifndef VECTOR_HPP
#define VECTOR_HPP

// Calculate the square root of a positive floating point number
float sqrt(float a);
// Calculate the length of vector with n elements. A vector is just
// an ordered list of components, the same as an array!
float mag(float vec[], unsigned int n);
// Calculate the length of a single float
float mag(float v);

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* vector.cpp */
#include "vector.hpp"

// We can assume that a is positive for this exercise because
// we're only ever passing it a positive value, but in a real
// application your sqrt function should error if a < 0
float sqrt(float a)
{
    // Try a / 2 as the initial guess
    float x = a / 2.0f;

    // Newton Rhapson will fail if x is close to zero
    // because we divide by x, but sqrt(x) = 0 in that case
    if(-0.00001 < x && x < 0.00001) return 0.0f;

    // Perform 10 iterations of Newton Rhapson
    // x <= (x + a/x) / 2
    for(int i = 0; i < 10; ++i)
    {
        x += a / x;
        x *= 0.5f;
    }

    return x;
}

float mag(float vec[], unsigned int n)
{
    // Iterate over every component and add its square
    // to the sum
    float sum = 0.0f;

    for(int i = 0; i < n; ++i)
    {
        sum += vec[i] * vec[i];
    }

    // Square root the sum and return it
    return sqrt(sum);
}

float mag(float v)
{
    // A single float is just a vector with one component,
    // so calling the vector mag with n = 1 would give the
    // required result, however sqrt(v*v) is simply the
    // positive version of v, so really we just need to
    // check for a negative number and make it positive
    if(v < 0) return -v; // Negative negative is positive
    else return v;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* main.cpp */
#include <iostream>
#include "vector.hpp"

int main()
{
    // Create some vectors
    float aVec[] = { 2.0f, 1.0f, 2.0f };
    float bVec[] = { 2.0f };
    float b = 2.0f;
    float cVec[] = { 1.0f, 1.0f };

    // This should be 3
    std::cout << mag(aVec, 3) << std::endl;
    // This should be 2
    std::cout << mag(bVec, 1) << std::endl;
    // And this should be the same
    std::cout << mag(b) << std::endl;
    // This should be about 1.41421
    std::cout << mag(cVec, 2) << std::endl;

    return 0;
}