5. Functions, Operators, Lambda Expressions, Function Overloading and Variadic Templates#

In the last section we learned about variables and types, and a little bit about the type system of C++. We made an analogy analogy to baking that juxtaposed types and recipe ingredients, and values with quantities of those ingredients. We will continue the analogy by juxtaposing functions as the specific instructions you have to follow to bake you cake (or bread). A cake (program) is uniquely determined by its ingredients, the quantity of each ingredient, and a perscription for how to combine these ingredients. One code even say that the oven is the compiler! But enough of a diagression.

In this chapter, we will learn all about functions. In C++, whatever isn’t a variabe is probably a function. We will discuss the syntax of declaring, defining, overloading and templating functions. We will finish by writing the Print function that was already introduced in the last section, and will be prominantly featured throughout the rest of these notes.

5.1. The Vanilla Function#

A function has four parts:

  1. The type declaration - this can include the template paramters, the declaration qualifiers, return type and attribute specification;

  2. The function name - which follows the same name converntions as variables;

  3. The argument list - a comma separated list of declaration qualifiers, variable types, reference qualifiers, and (optionally) the variable name; and

  4. The function body - the recipe the function is supposed to execute.

Rule: Function declarations

A non-templated function declaration

type_specifier type_declaration function_name(argument_list)
{
    function_body
}

A templated function declaration

template<typename template_name>
type_specifier type_declaration function_name(argument_list)
{
    function_body
}

Functions can grossly be grouped into two categories, in terms of the their function in a program. These are

  1. Pure functions - functions that only consume input and produce output; and

  2. Impure functions - functions that can have side effects.

Side effects arise when the function modifies some other variable (we will use the word state henceforth) in the program. An example of a pure function is

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

Here, the function simply takes to integers a and b as arguments, and returns their sum. An example of a function that preforms a side effect is

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

Here, the state of c is modified to store the result of the sum and the sum is returned. One can imagine that a function’s job is to check the state of one or more variables, and flag another for later action. Two examples would be:

  1. Redrawing the screen after the mouse has been moved or key pushed, so the screen reflects the system’s new state

  2. Indicating to a mesh refiner, for adaptive mesh refinement simulation, to refine a certain grid point before moving to the next time step.

5.1.1. The int main function#

A special function in all C++ programs is the int main function. Every program needs exactly one for the program to be compiled. Should the compiler not find this symbol, then it will complain and fail.

The int main function can have to forms:

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

The agrc variable stores how many arguments were passed to the program from the command line. Assuming the program’s name is prog, a call with multiple arguments would look like

./prog arg1 arg2

The passed arguments are stored in argv, which is an array of length argc of null-terminated strings. The name of the program is always stored in argv[0] and the passed arguments (if argc > 1) start at argv[1]. Parsing of command line arguments is the responsibility of the developer, though well establishd library have been developed for this purpose and can be found on github.

Note

Your program does not need to take command line arguments. A benefit from taking command line arguments can be that not needing to recompile a program just because you want it to be change its behavior.

The return value of the int main function is default to zero (that is, if no return statement is give, it will automatically add a return 0; to you int main body). Non-zero return values, which we will refer to as exit codes typically indicate some error that occured in the program.

Some facts to remember about int main:

  1. It cannot be called by other functions;

  2. A multi-source project can can have exaclty one;

  3. It cannot be overload (see below); and

  4. It can only have to two signatures described above.

5.2. Templated functions#

As stated in the last chaper, templates allow you write code that is generic in types. To appreciate this, we need to note that that C++ is a strongly typed language, meaning the following code shouldn’t compile (I say shouldn’t because all primitive types can be converted into each other).

float add(float x, float y) { return x + y; }

int main() 
{
    char x{ 'h' };
    char y{ 'e' };

    char z{ add(x, y) }; // Won't compile because `add` expects `float` 
}

The strong typing becomes more apparent once we used user-defined types, or classes.

A way of writing a generic/templated addition function is

template<typename T>
T add(T a, T b) { return a + b; }

int main()
{
    int a{ 1 };
    int b{ 1 };
    int c{ add(a, b) };    // OK!

    double x{ 1.0 };
    double y{ 1.0 };
    double z{ add(x, y) }; // OK!

    int j{ add(a, x) };    // FAILS! Compiler fails to deduce template paramter due 
                           // to conflicting variable types `int` and `double`
}

Underneath the hood, the compiler reads through the source code, and for every call the templated function, tries to generate a version of add with the types specified.

5.2.1. Single template parameters#

The simplest type of templated function is the single template parameter function. For an example see the definition for the add function above. Note that in calling the function, we do not need to use any syntax to indicate what the need type of the template is. Instead, the compiler figures this out for us; if it could not, then it would give us a compilation error.

Using the example above, we could explicitly indicate what type the add function shoud admit by adding the chevron syntax

template<typename T>
T add(T a, T b) { return a + b; }

int main()
{
    int a{ 1 };
    int b{ 1 };
    int c{ add<int>(a, b) }; 

    double x{ 1.0 };
    double y{ 1.0 };
    double z{ add<double>(x, y) };
}

It should now be more obvious why the call add(a , x) does not work. We cannot write add<int>(a, x) because x is of type double, and we cannot write add<double>(a, x) because a is of type int.

Rule: Single template function declaration

A single template parameter function declaration

template<typename t_param_name>
type_specifier type_declaration function_name(argument_list)
{
    function_body
}

where t_param_name needs to appear in argument_list. We can equally replace typename with class, the syntax is equivalent.

5.2.2. Multiple templates parameters#

You can provide multiple template parameters, by separating the the typename declarations with commas. A quick example might be printing the key and a value in a dictionary

#include <unordered_map>    // C++ name for a dictionary type
#include <iostream>         // For `std::cout` to print to the terminal

template<typename Key, typename Value>
void print_dictionary(std::unorder_map<Key, Value> const& dict)
{
    for (auto const& [key, value] : dict)
        std::cout << key << ": " << value << std::endl;
}

int main()
{
    std::unordered_map<char, int> dict_1{
        {'a', 1},
        {'b', 2},
        {'c', 3}
    };
    print_dictionary(dict_1);

    // First five digits of pi
    std::unordered_map<int, int> dict_2{
        {1, 3},
        {2, 1},
        {3, 4},
        {4, 1},
        {5, 5}
    }
    print_dictionary(dict_2);
}

Rule: Multiple template function declaration

A multiple template parameter function declaration

template<typename t_param_name_1, typename t_param_name_2>  // etc
type_specifier type_declaration function_name(argument_list)
{
    function_body
}

where t_param_name_# needs to appear in argument_list. We can equally replace typename with class, the syntax is equivalent.

5.2.3. Variadic template parameters#

A very useful, and very powerful use of template programming are the variadic templates. This, in short, means, functions with a variable number of template arguments. A pair of convience functions that we will use time and time again in our examples are our print and println function. Its definitions looks like this

#include <iostream>

template<typename... Args>
void print(Args&&... args)
{
    ((std::cout << std::forward<Args>(args) << " "), ...);
}

template<typename... Args>
voif println(Args&&... args)
{
    print(std::forward<Args>(args)...);
    std::cout << std::endl;
}

It is worth breaking down the synatx here. The ellispe denotes variadic: variadic programming is also present in the C programming language and implemented via macros. When they appear in the template declaration or argument list, they indicate that an arbitrary (limited by computer chip) number of template types/arguments are to be expected, and are referred to as a parameter pack. When appear in the body of a function, they indicate that a template parameter pack is being unpacked; expression that contain the syntax ((), ...) are called folding expressions. The double ambersand && indicates that the arguments should be interpreted as r-value references unless const-qualified (more about this in Chapter {reference}). Lastly, the std::forward function, also a template function, also converts values to r-values.

Rule: Variadic template function declaration

A variadic template parameter function declaration

template<typename... Args>
type_specifier type_declaration function_name(argument_list)
{
    function_body
}

where Args needs to appear in argument_list.

You can declare functions with two variadic template arguments. There is no immediate use-case that comes to mind for me, but you can feel free to look up examples. The important part is that the first parameter pack has to declared in the function call using chevrons, and the second is deduced from the arguments passed to the function.

5.2.4. Template template parameters#

Another variable powerful form of template programming is template template functions. These will be particularly useful after our next chapter where you will learn about classes and structures. For the sake of our demonstration, assume you have a container that stores some unkonwn number of arbitrary type T or that you have an array of N things of type T. We will call these objects Container and Array respectively. We can then write generic functions that print these objects (provided some internals exist in the class, which we will cover in the next Chapter).

#include <iostream>

template<template<typename> class Container, typename T>
void print_container(Container<T>&&... container)
{
    for (auto const& entry : container)
        println("   ", entry);
}

template<template<typename, auto> class Array, typename T, auto N>
void print_container(Array<T, N>&&... array)
{
    for (auto const& element : array)
        println("   ", element);
}

Here, we have used auto to represent a non-type template parameter, as we expect N to be an number not a type. In practice, the array declaration may look like template<typename T, std::size_t N> class Array;, where it is now clearer that N is not a type but a constant.