Functions

Programming Workshop 2 (CSCI 1061U)

Faisal Qureshi

Faculty of Science, Ontario Tech University

http://vclab.science.uoit.ca


Introduction

Functions are building blocks of programs. We can characterize functions as input-process-output units. Each functions takes an input, processes it, and returns a value. Functions are sometimes also referred to as procedures, subprograms, subroutines, and methods. In C++, we will refer to functions simply as “functions.” The inputs to the function are often called function arguments or function parameters. A function can return at most 1 value.

Consider the following C++ function that adds three numbers.

int sum(int a, int b, int c)
{
    int sum = a + b + c;
    return sum;
}

We can understand the above code as follows:

Functions that do not return a value

It is also possible for a function to not return a value. Consider the following function that prints the value of the input passed to it.

void print_value(double a)
{
    std::cout << a << std::endl;
}

Notice that the return type is set to void. This return type indicates that the function doesn’t return a value.

Functions that do not take an input

It is also possible to write a function that doesn’t take an input. Consider the following function

double value_of_pi()
{
    return 3.14159;
}

Function value_of_pi takes no input. It simply returns a value of type double.

Functions that neither take an input, nor return a value

Sometimes a function takes no input and returns no value. Consider the following example

void say_hello()
{
    std::cout << "Hello" << std::endl;
    return;
}

Similar to function print_value shown above, this function doesn’t return a value. We can still use the return keyword as shown above.

Predefined functions

The real power of C++ comes from the fact that it comes with libraries that are full of pre-defined functions that we can use. In order to use these pre-defined functions, we have to use the appropriate #include statement. Consider the following program

#include <iostream>

int main(int argc, char** argv)
{
    std::cout << "Hello world" << std::endl;
    return 0;
}

The above program will give an error if we do not include the statement #include <iostream>. This is because we are using the std::cout (operator) that is defined in iostream header file (more on this later).

Lets consider another example

#include <iostream>
#include <math>

int main(int argc, char** argv)
{
    std::cout << "Sq root of 25 is " << sqrt(25.0) << endl;
    return 0;
}

Notice that we included the statement #include <math>. This is because we are using the pre-defined function sqrt, which is defined in the math header file.

Using functions

Three pieces to using a funtion are:

The folowing examples illustrates these pieces.

Function definition and function calls

In order to use a function, we first need to define it, i.e., we need to specify its inputs, what it does, its return value, and then we call it. Lets consider the following code

#include <iostream>

// funtion definition
int sum(int a, int b, int c)
{
    int s = a + b + c;
    return s;
}

int main(int argc, char** argv)
{
    int a = 1, b = 20, c = 34;
    
    // function call
    int t = sum(a, b, c); 

    std::cout << "Sum of a, b, and c is " << t << std::endl;

    return 0;
}

In the code above, ln. 4–8 define the function. These include function signature (ln. 4) and function body (ln. 5–8). Later in the code the function is called on ln. 15. Note that the function is only executed if it is called.

It is perfectly legal to call a function multiple times. It is this feature of a function that enables re-use.

Function declaration

The following code will give a compile-time error.

#include <iostream>

int main(int argc, char** argv)
{
    int a = 1, b = 20, c = 34;
    
    // function call
    int t = sum(a, b, c); 

    std::cout << "Sum of a, b, and c is " << t << std::endl;

    return 0;
}

// funtion definition
int sum(int a, int b, int c)
{
    int s = a + b + c;
    return s;
}

The reason being that the compiler hasn’t seen the function when it encounters the function call at ln. 8. When compiler encounters ln. 8, it has no idea what sum refers too. We can avoid this by declaring the function beforehand. This way compiler would know that sum is a function, which will be defined later. We can declare the function by adding the following statement before the main funciton.

int sum(int, int, int);

Or to make things clearer

int sum(int a, int a, int a);

A bit about include files

It turns out that among other things, include files, contain function declarations. So now we know why we need to use the include directive.

A few things to remember

In C++, all functions are equal. This means that we cannot define a function inside another function. Keyword return is used to return a value back to the function that is calling the function. In the example above, function main calls function sum. So when return is encountered in sum, the value (and the program control) is returned to its caller function (which is function main).

The main function

In C++, main is a special function. Each program must have one and only one main function. The program is exited (i.e., shutdown) when main function calls return. The operating system calls the main function. The value returned by the main function is passed on to the operating system.

Scoping rules for functions

Variables declared inside the body of the function are available only within that function. This allows us to use the same variables names in different functions. The function arguments are also available in the function body as variables. This is way we need to explicitly name function inputs when defining functions; where as, we don’t need to explicitly name function inputs when declaring functions (we still need to provide the types for function inputs). In C++, function arguments are passed by value. This has an important consequence. Any changes made to function arguments inside a function are not visible to the caller function.

Consider the following code

#include <iostream>

int add(int a, int b)
{
    int result = a + b;
    return result;
}

int sub(int a, int b)
{
    int result = a - b;
    return result;
}

void multipy_by_2(int a)
{
    a = a*2;
}

int main()
{
    int a = 1, b = 3;
    
    int a_plus_b = add(a, b);
    int a_minus_b = sub(a, b);
    
    multiply_by_2(a);

    return 0;
}

Notice that we are using variable result in functions add and sub. There is no danger though, each function defines its own result variable that is only visible within this function. Also, notice that we changed the value of argument a in function multiply_by_2.

This, however, doesn’t effect the value of a in function main. Later, we will look at techniques to break this restriction. For now, can you think of reasons why you would want the changes made to an argument be visible to the caller function?

Global constants and global variables

Global constants or global variables are declared “outside” the function body. The global variable and constants are available to every function within the file.

We use const keyword to declare a global constant as follows:

const double PI = 3.14159;

Global variables can be defined as follows

int total;

In general it is not a good idea to use global variables.

Block-scope

Variables declared inside a block (i.e., between a pair of {' '}) have block-scope. These variables are only visible inside that block. Consider the following two cases.

#include <iostream>

int add(int a, int b)
{
    int result = a + b;
    return result;
}

int main()
{
    int a = 1, b = 3;
    std::cout << "Outside: " << sum(a, b) << std::endl;

    {
        int a = 3;
        std::cout << "Inside: " <<  sum(a, b) << std::endl;
    }

    return 0;
}

When you execute this program, you will get teh following output

Outside: 4
Inside: 6

This is because the variable a defined inside the block has no relationship to the a defined outside the block. We often use this feature in for loops.

for (int i=0; i<10; ++i)
{
    std:cout << i << std::endl;
}

Here variable i is only defined within the for loop block.

Blocks can be nested as well.

Function arguments (call-by-value vs. call-by-reference)

There are two methods of passing arguments. The above examples all use call-by-value. Value of the argument is copied and then passed into the function. We have already observed the consequences. If an argument value is changed within the function, this change is not reflected outside of the function (i.e., in the caller function).

Another way to call a function is to use call-by-reference. Here, the address of the argument is passed into the function, What this means that when a value is changed within the function, the change is reflected outside the function.

The following example uses call-by-value

void inc(int a)
{
    a += 1;
}

The following example, on the other hand, uses call-by-references

void inc(int& a)
{
    a += 1;
}

We use the ampersand & to indicate call-by-reference.

You should use caution when using call-by-reference, since the changes made in a function are visible outside of it. Still when used properly, this feature allows for great flexibility in program design.

It is also more efficient to pass arguments using call-by-reference. Recall that data is copied when passing arguments using call-by-value. Call-by-reference provides an elegant solution to unnecessary data copy. It is possible to indicate that the passed value should not be modified within the function using a const references as follows.

int add(const int& a, const int& b)
{
    return a + b;
}

In the above code, we can use a and b as r-values, but not as l-values. In other words, we are not allowed to assign values to a or b.

Function overloading

In C++ it is possible to have two functions with the same name, but different arguments. This is called function overloading. It is important to have different arguments, othwerise compiler will have no way of disambiguating the two functions when a function is called. Note that return type cannot be used for this disambiguation. Why is that?

Consider the following code

int sum(int a, int b)
{
    return a+b;
}

int sum(int a, int b, int c)
{
    return a+b+c;
}

This is perfectly valid. The compiler can easily distinguish between the two functions based upon the number of arguments at call time as follows

// Calling first function
int result = sum(1,32);

// Calling second function 
int result = sum(1,3,44);

The reason we can’t use the return type for function disambiguation is because we are allowed to call a function without mentioning its return type as follows

sum(1,2)

Overloading resolution

Looks for exact match. Failing that looks for compatible match where automatic type conversion is possible.

Type can be promoted (int to double) or demoted (double to int). In the first case, no data is lost. While in the second case there is data loss.

Default arguments

Consider the following code

int volume(int length, int width=1, int height=10) 
{
    return length*width*height;
}

We can call this function as follows

volume(3);

Note that we didn’t need to specify the values for width and height. In C++ each arguments are position based. So the following is not allowed.

int volume(int length=2, int width, int height=10) 
{
    return length*width*height;
}

Why? Because if we call it as follows

volume(3);

The compiler wouldn’t know if 3 is for length or for width.

With practice you will figure out how best to use function overloading alongwith default arguments.

Should I write my own functions

Yes! Functions increase readability, encourage re-use, and allow you to write large programs. You simply can’t avoid writing functions. As you gain more experience, breaking down a large program into functions will be become second nature. Functions also provide procedural abstraction. The caller only needs to know what a function does, not how it does it. This is a powerful feature of functions. This allows you to swap one function for another as long as function signatures match (i.e., same name, same return type, same arguments). The caller doesn’t know, nor cares, about function body.

Credits