This lesson is still being designed and assembled (Pre-Alpha version)

Function Calling and Arguments

Overview

Teaching: 30 min
Exercises: 5 min
Questions
  • TODO

Objectives
  • TODO

Prerequisites

  • TODO

Function overview

Like in python, functions are a fundamental building block in C++.

Let’s refresh our memories of some of the terminology around functions. Note that this terminology is the same as in Python, although the return type is not defined with the function name..

The void Keyword

void can be used in the signature to signify a function takes zero arguments, and can also be used as the return type to signify the function does not return anything. See, for example, our main function.

void say_something(void)
{
    // No return type, no arguments!
    std::cout << "Something!" << std::endl;
}

The const Keyword

In C/C++, it is possible to mark a variable as constant, such that after setting, the value cannot be changed. Marking a variable as constant can also enable some performance improvements, although these are typically very small. However, they are primarily used to signify intent to other programmers, and can prevent some mistakes.

To create a constant variable, the keyword const is placed before the type.

const double pi = 3.1415; // Constant double
const std::string my_string = "Hello world!"; // Constant string
my_string = "Another string"; // Error - my_string is const!

Because the constant variable cannot be changed, it must be set at the same time it is declared. Otherwise, you would never be able to set the value!

const int i = 123; // Declare and set
const int j; // j can never be changed after this, so this is invalid.

Constant variables can be very useful in some cases. For example, for storing physical constants or common strings. As we will see, they are most often used for declaring objects passed to functions as const.

Constant references work in a simiar way. Data cannot be changed through a constant reference.

A constant reference can refer to a constant or non-constant variable. However, a non-const reference can only refer to a non-constant variable. Otherwise, we would be able to change constant data just my making a reference to it.

Exercise

What happens when you try to make a non-const reference to const data?

int main(void)
{
   const double d = 1.123;
   double & d2 = d;
   return 0;
}

Solution

A compiler error, of course!

test.cpp: In function ‘int main()’:
test.cpp:4:19: error: binding reference of type ‘double&’ to ‘const double’ discards qualifiers
    4 |     double & d2 = d;
      |    

In C++, the const keyword is called a qualifiers. The compiler is telling you that making the reference discards the const qualifier. volitile is another qualifier, however it is very rarely used in scientific computing.

Argument passing by copy

Now that we have gone over void and const, as well as pointers and references in previous lessons, lets take a look at how these concepts can be exploited when calling functions.

This will mark a large departure from Python, and a significant source of confusion for those coming from Python. Python only has one real way to pass arguments, while C++ has several

In C++, unless otherwise specified, arguments are passed into functions by copying the contents. The two main things to remember about passing via copying is that

  1. This can be expensive for large data types (think a matrix or an std::vector with many elements)
  2. Changes made to the data within the function are not reflected outside

This last point can be a benefit, however, and can protect you from making accidental mistakes.

Lets start by writing a function to convert temperatures from Fahrenheit to Celsius.

#include <iostream> // for std::cout, std::endl

void convert_F_to_C(double temperature)
{
    temperature = (temperature - 32.0)*(5.0/9.0);
}

int main(void)
{
    double temperature = 68.1;
    convert_F_to_C(temperature);

    std::cout << "Temperature is " << temperature << std::endl;
    
    return 0;
}

Here is the output

Temperature is 68.1

You will notice that the value of temperature in the main function does not change, even though we changed it inside of convert_F_to_C. That is becuase the variable temperature inside convert_F_to_C is a copy of the temperature variable in main.

Discussion

What would be a better way of writing the temperature function?

Argument passing by reference

Lets say you really needed the convert_F_to_C function to modify the temperature variable in-place and have that reflected outside the convert_F_to_C function. We can do that via passing by reference.

You should remember the idea behind references from the previous lesson. A reference is similar to a pointer, and simply refers to an existing variable. In this case, when the function is called, a reference is created to the variable being passed into the function. This variable exists outside the function, and so this allows the function to modify that variable’s contents through the reference.

Passing by reference is very common in C++. In general, this should be the default way to pass in data the will be changed in the function.

#include <iostream> // for std::cout, std::endl

void convert_F_to_C(double & temperature)
{
    temperature = (temperature - 32.0)*(5.0/9.0);
}

int main(void)
{
    double temperature = 68.1;
    convert_F_to_C(temperature);

    std::cout << "Temperature is " << temperature << std::endl;
    
    return 0;
}

The example above is the same as the previous example, except with the addition of the & into the function signature of convert_F_to_C. The output shows that this makes all the difference.

Temperature is 20.0556

Exercise

Print the address of the variable inside and outside the convert_F_to_C function.

Solution

#include <iostream> // for std::cout, std::endl

void convert_F_to_C(double & temperature)
{
   std::cout << "In convert_F_to_C: " << &temperature << std::endl;
   temperature = (temperature - 32.0)*(5.0/9.0);
}

int main(void)
{
   double temperature = 68.1;

   std::cout << "In main: " << &temperature << std::endl;
   convert_F_to_C(temperature);

   std::cout << "Temperature is " << temperature << std::endl;
   
   return 0;
}
In main: 0x7ffe27c761e0
In convert_F_to_C: 0x7ffe27c761e0
Temperature is 20.0556

The address will likely differ from my example, however the addresses should be the same.

Passing by constant reference

Passing by constant reference allows for larger amounts of data to be passed without incurring a performance penalty due to copying, while ensuring that the data is not modified within the function. This is a very common idiom that you will see in C++ packages.

If you try modifying the constant reference inside the function, it will cause a compiler error.

#include <iostream> // for std::cout, std::endl

double convert_F_to_C(const double & temperature)
{
    temperature = (temperature - 32.0)*(5.0/9.0);
}

int main(void)
{
    double temperature = 68.1;
    temperature = convert_F_to_C(temperature);

    std::cout << "Temperature is " << temperature << std::endl;
    
    return 0;
}

Comparison to Fortran

A const reference in a Function signature is similar to an argument being declared as intent(in) in Fortran. A plain non-const reference would be similar to intent(inout).

Argument passing by pointer

An alternative to passing by reference is passing by pointer. For C++, this is not generally recommended much anymore, however is still very common in C.

To pass by pointer, mark the argument type in the function signature as a pointer type (with a *). When calling the function, you pass the address of the variable. The function directly accesses the variable with the * operator.

While not as common in C++, it is sometimes used to pass around arrays allocated with new, after which elements are accessed with *. This is very awkward for many other uses, where references are preferred.

#include <iostream> // for std::cout, std::endl

double convert_F_to_C(const double * temperature)
{
    temperature = (temperature - 32.0)*(5.0/9.0);
}

int main(void)
{
    double temperature = 68.1;
    temperature = convert_F_to_C(&temperature);

    std::cout << "Temperature is " << temperature << std::endl;
    
    return 0;
}

Const correctness

As we have seen, a function that takes a const reference can accept both const and non-const data. However, a function taking a non-const reference is restricted to only taking non-const objects. This leads to a general rule of writing function arguments: make your reference (and pointer) arguments const unless you actually intend to modify them. This way, your function is usable in all cases, since the calling function may have const data it needs to pass in.

This thinking is called const correctness. If you do not do this, you will eventually write yourself into a corner and will find yourself refactoring your entire project. So it is much easier to start thinking about const from the very beginning. After some experience, it will be second nature.

Making a choice

So now we have several ways to pass arguments to a function. Which should we use?

Function overloading

Now we come to a feature of C++ that is not available in C or in Python. In C++, we can have multiple functions with the same name, as long as the function signature is different. The compiler will determine which function will be used based on the arguments passed to the function.

For example, we can have to convert_F_to_C functions - one that takes just a double, and the other that takes a std::vector<double>.

#include <iostream> // for std::cout, std::endl
#include <vector>

double convert_F_to_C(const double & temperature)
{
    temperature = (temperature - 32.0)*(5.0/9.0);
}

std::vector<double> convert_F_to_C(const std::vector<double> & temperatures)
{
    std::vector<double> converted;
    for(size_t i = 0; i < temperatures.size(); i++)
    {
        double temp = convert_F_to_C(temperatures[i]);
        converted.push_back(temp);
    }

    return converted;
}

int main(void)
{
    double single_temperature = 68.1;

    // Calls the single double version
    single_temperature = convert_F_to_C(single_temperature);

    std::vector<double> temperatures;
    temperatures.push_back(0.0);
    temperatures.push_back(-40.0);
    temperatures.push_back(123.4);

    // Calls the version taking a vector
    std::vector<double> new_temperatures = convert_F_to_C(temperatures);

    return 0;
}

Argument-dependent lookup

The process by which C++ determines the proper function to call based on arguments is extremely complex. It has to take into account not just argument types, but (implicit) conversions, custom conversions, and more advanced C++ features such as templates. In general, the compiler will do the correct thing, and if there is ambiguity, will require further clarification on your part.

Default Arguments

A default for an argument can be supplied in the function signature. However, the given default argument must be of the correct type (not like in Python, where you can set the argument default to be None).

#include <iostream>

double convert_F_to_C(double temperature = 0.0)
{
    return (temperature - 32.0)*(5.0/9.0);
}

int main(void)
{
    double temperature = 68.1;
    convert_F_to_C(temperature);

    std::cout << "Temperature is " << convert_F_to_C(68.1) << std::endl;
    std::cout << "Temperature is " << convert_F_to_C() << std::endl;
        
    return 0;
}
Temperature is 20.0556
Temperature is -17.7778

Exceptions

TODO

File input/output (fstreams)

TODO

std::pair

TODO

Key Points

  • TODO