Functions
Functions
The function (aka procedure) is a one of the defining aspects of proceedural programming. It
allows you to package up some code defining a particular operation into a
re-useable function can can take zero or more arguments and (optionally)
return a value.
Declaring and calling funcitons
The following function prototypes declare two functions: one called
my_func
that takes two parameters of type double
{.Cpp} and returns a
variable of type double
{.Cpp}, and one called main
that takes no parameters
and returns an int
{.Cpp}:double my_func(double x, double y); int main();
The function prototype tells the compiler about function's name, return type,
and parameters. You must declare the function before you can use it, like so:
#include <iostream> double multiply(double x, double y); // function prototype int main() { double a = 1.0, b = 2.0, z; z = multiply(a, b); std::cout << a << " times " << b << " equals " << z << '\n'; return 0; } double multiply(double x, double y) // function definition { return x * y; }
A function may also return no value, and be declared as
void
{.Cpp}.#include <iostream> void output(int score, int passMark); int main() { int score = 29, pass_mark = 30; output(score, pass_mark); return 0; } void output(int score, int passMark) { if (score >= passMark) std::cout << "Pass - congratulations!\n"; else std::cout << "Fail - better luck next time\n"; }
Any variables that are used in the function must be declared as normal.
For example:
double multiply_by_5(double x) { double y = 5.0; return x * y; }
Recall the rules about scope, the scope of
y
lasts until the end of the
function (the last curly bracket) after which y
is removed from memory and is
no longer available.Pass by value
A function can only change the value of a variable inside the function, and not
in the main program. This is because, by default, variables are passed by value, and the function
only sees a copy.
Changes in this copied variable have no effect on the original variable, for
example the function
no_effect
has no effect on the x
variable passed into it in main
:#include <iostream> void no_effect(double x) { x += 1.0; } int main() { double x = 2.0; no_effect(x); std::cout << x << '\n'; }
Pass by reference
A common way of allowing a function to change the value of a variable outside the
function is to use references. You can do this by adding
the
&
symbol before the variable name in the declaration of the
function and the prototype.#include <iostream> void add(double x, double y, double& rz); int main() { double x = 1.0, y = 2.0, z; add(x, y, z); std::cout << x <<" plus "<< y <<" equals "<< z <<'\n'; return 0; } void add(double x, double y, double& rz) { rz = x + y; }
Swap Two Numbers
Write a function that accepts two floating point numbers (using references), and
swaps the values of these numbers.
Function overloading
When a function is declared, the return type and parameter type
must be specified.
If a function
mult
is to be written that multiplies two numbers, we
would like it to work for floating point numbers and for integers.This can be achieved by function overloading.
More than one function
mult
can be written - one that takes two
integers and returns an integer, one that takes two floating point
numbers and returns a floating point number, etc.float mult(float x, float y) { return x * y; } int mult(int x, int y) { return x * y; } int main() { int i = mult(7, 10); float f = mult(21.5f, 14.5f); }
Scalar (dot) product
Write a function that returns the scalar (dot) product of two
std::array<double, 3>
vectors. Overload this function to multiply two scalar
double
values.Return values
Functions can have no return value
void print_this(int x);
a single return value
int get_constant();
multiple return values via a
std::tuple
std::tuple<std::string, float> get_student_and_grade();
or can optionally return value (i.e. either a value or nothing) via
std::optional
std::optional<std::string> read_file_if_exists(const std::string& filename);
The use of
std::optional
here tells the caller that the return
std::string
might not exist (e.g. if the file does not exist or cannot be
opened for reading) and that this possibility must be dealt with after calling
the function. For example:const std::string filename = "data.txt"; if (const auto contents_opt = read_file_if_exists(filename)) { std::cout << *contents_opt << std::end; } else { std::cerr << "Cannot read file " << filename << std::end; }
Errors and Exceptions
It is normally neccessary to deal with errors that occur within a function in
such a way that the caller of that function is aware of the error and can deal
with it (if possible), or fail gracefully (perhaps clean up resources like an
open file for example). In the previous section we saw one approach to dealling
with an error, which is to return an optional value from the function. Another
approach is to use C++ exceptions.
Let us define a function for solving a particular problem (e.g. a root-finding
problem). This function has an input argument
x
of type double
, but the
solver we are writing can only solve the given problem for . Furthermore,
even if it is possible that the function fails to find a solution to
the problem.Since we have two possible points of failure, we decide to use exceptions to
make the caller aware of any failures, and what in particular has gone wrong.
double solve_problem(const double x) { if (x <= 2) { throw std::invalid_argument("x must be greater than two"); } /// ... solve problem here if (!success) { throw std::runtime_error("solver failed"); } return result; }
Both
std::invalid_argument
and std::runtime_error
are exception classes in
the standard library. When
the program gets to the throw
{.cpp} expression, excecution is halted and
control flow immediately works backwards up the current call stack until a
catch
expression is encountered with an argument compatible with the
exceptions thrown (here either std::invalid_argument
or std::runtime_error
).
If none is found then the program halts with an error.Below is an example of how you might call
solve_problem
and handle the
possible errors with a try-catch expression:int main() { double solve_for_x = 1.456; try { solve_problem(solve_for_x); } catch (std::invalid_argument err) { // oh no, double it and try again solve_problem(2 * solve_for_x); } catch (std::runtime_error err) { // Fall back and try 10.0, I know this one works! solve_problem(10.0); } catch (std::exception err) { // unknown error, just print it out and exit std::cerr << err << std::endl; return -1; } }
Here we have three
catch
blocks, corresponding with different exceptions we
want to handle. The first two are the ones we saw in the definition of
solve_problem
. The third is the base exception class in the standard library,
so any exception in the standard library (or any exception derived from one of
these) will be caught.Templated functions
Templates in C++ introduce compile-time polymorphism (Polymorphism is a
programming concept meaning to provide a single interface for entities of
differing types). Templates can be used to where the same code may need to
repeated for different values or for different types. For example, say we had a
function
get_min
that could accept either double
or int
via overloading:double get_min(double a, double b) { if (a < b) {return a;} return b; } int get_min(int a, int b) { if (a < b) {return a;} return b; }
This is rather cumborsome as we have to repeat the implementation of the two
overloaded functions. Instead, we can use the
template
{.Cpp} keyword to
produce as many functions as may be required:template <typename Number> Number get_min (Number a, Number b) { if (a < b) { return a; } return b; } int main(void) { int i = get_min<int>(10,-2); double d1 = get_min<double>(22.0/7.0, 3.14159265359); double d2 = get_min(22.0/7.0, 3.14159265359); }
Each use of the templated function (i.e.
get_min<int>()
, get_min<double>()
)
causes the compiler to generate a new version of the get_min
function with the
template argument Number
replaced by the type given by the template argument.Function template type deduction
Note: it is not always necessary to provide the typename when calling a
templated function, as long as the compiler can infer it:
int main(void) { int arg1 = 10; int arg2 = -1; std::cout << get_min(arg1,arg2) << std::endl; }
Multiple template arguments
You can list multiple template arguments one after the other. These can be types
(e.g.
typename T
{.Cpp}) or non-types (e.g. int N
{.Cpp})template <int N, typename T> T multiply_by_n (T a) { return N*a; } int main(void) { int i = 1; std::cout << multiply_by_n<2>(i) << std::endl; }
Scalar (dot) product continued
Rewrite your dot product function to take any two containers and that follow the standard container interface in C++. Your function should take three arguments:
- A start iterator for vector
- An end iterator for vector
- A start iterator for vector (vector is assumed to be the same size as vector )
Template your function on the iterator type for
Ta
, and the iterator type
for Tb
. If you like, you can perform the calculation of the dot product
using a fixed type double
. For an extra challenge, make sure you use the same
type contained in (hint: each iterator and container in the standard library
has a subtype value_type
that is the value type held by the container).