Chapter 7 - Notes

Functions

  • Allows one to compartmentalise and organise a program's execution logic

  • Can be viewed as a subprogram that optionally takes parameters and returns a value.

  • The syntax for defining a function is as follows:

Function Prototypes

A function prototype is a declaration of a function, in the following syntax:

RETURN_TYPE FUNCTION_NAME(PARAMETER_1_TYPE PARAMETER_1_NAME, ...); 
  • RETURN_TYPE: the type of the value returned from the function

  • FUNCTION_NAME: the name of the function that is being declared

  • PARAMETER_1_TYPE: the type of first parameter

  • PARAMETER_1_NAME: the name given for the parameter

The number of parameters that a function requires is optional, from 0 (no parameters) to 256 parameters.

Multiple parameters are specified separated by a comma (,).

Function Definition

A function definition is the actual meat and potatoes of a function, where the code to be executed is defined.

RETURN_TYPE FUNCTION_NAME(PARAMETER_1_TYPE PARAMETER_1_NAME, ...) {
    STATEMENT_1;
    STATEMENT_2;
    ...
    return VALUE;
}

Much like before, instead of ending the function prototype with a semicolon (;) it is followed instead by a statement block ({...}).

Within the block is the code that will be executed when the function is invoked.

Function Call & Arguments

  • The code defined as part of a function is not executed unless it is called/invoked.

  • If the function's definition contains parameters, then the function needs to be called with arguments.

Functions Without Return Types

  • Functions that do not return a value are defined as returning the type void.

  • return statements can still be used for such functions on their own (e.g. return;)

Functions Parameters with Default Values

Default parameters are parameters that assume a default value if one is not provided by the function caller.

void greet(std::string name = "stranger") {
    std::cout << "Hello there, " << name << std::endl;
}

int main() {
    greet("Andrew");  // prints 'Hello there, Andrew'
    greet();  // prints 'Hello there, stranger' 
}

Recursive Functions

  • Recursive functions are functions that calls itself.

  • Such functions should have a clearly defined base case where it returns without invoking itself again.

  • The function should get closer to the base case with each recursive call.

What happens if a recursive function does not have a base case, or the base case never gets executed?

Function Overloading

Multiple functions can share the same name; what has to be unique is the function prototype.

What makes up a function prototype?

float sum(float a) {
    return a;
}

int sum(int a) {
    return a;
}

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

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

Arrays as Function Parameters

  • Array parameters can be specified with the ELEMENT_TYPE PARAMETER_NAME[] syntax.

void printNumbers(int numbers[], int length); 
  • The length parameter is supposed to denote the length of the array that is passed in, so that you may enumerate over the array without stepping out of bounds.

Array decay

Typically we define an array like this:

int numbers[10] = {0};

This defines a variable called numbers, with the type int[10] - meaning that the length of the array is embedded into variable type; which is why within the same scope we are able to use a range-based for-loop to enumerate over the array.

When creating a function that accepts an array as a parameter, the array is now of the type int[], meaning it has lost information about its size.

This is known as array decay – the array's type has decayed from one that is more specific (int[10]) to one that is less specific (int[]).

To circumvent this, we pass in the length of the array as another parameter to the function.

Reference Function Parameters

Reference parameters allow us to modify the value of an argument from within the function call

This is done by including an ampersand (&) after the type of the parameter.

void foo(int& x) {
    x = 10;
}

int a = 5;
std::cout << a << std::endl; // This prints 5

foo(a);  
std::cout << a << std::endl; // This prints 10

This is useful for:

  • Emulating multiple return types – using multiple reference variables, we can return more than one value from a function.

  • Efficiency – if the size of the parameter is large, we can gain some efficiency by passing them by reference.

  • Changing values – if we need to alter the value of an argument from inside a function

Inline Functions

Preamble

When a function in a program is invoked, there is a couple of 'setup' steps that the program must perform behind the scenes before the code in the function is actually run:

  • Push all arguments to the stack to be used as parameters

  • Store the instruction to be executed after the function exits

  • Jump to the function that is to be executed

And once the code in the function is all run:

  • Store the return value of the function

  • Jump back to the instruction that was previously stored

The overhead of this is usually negligible, but for a simple/small function that will be executed in a tight loop this overhead might impact the performance of the program.

Inlining Functions

The keyword inline when used when defining a function hints to the compiler that the function is to be expanded inline whenever it is called.

inline double multiply(double a, double b) {
    return a * b;
}

int main() {
    int a, b;
    std::cout << "Enter two numbers: ";
    cin >> a >> b;
    std::cout << "The product is: " << multiply(a, b) << std::endl;
}

In the above example, the function multiply is expanded inline, and becomes equivalent to:

int main() {
    int a, b;
    std::cout << "Enter two numbers: ";
    cin >> a >> b;
    std::cout << "The product is: " << a * b << std::endl;
}

The inline keyword is only a hint to the compiler that the function should be inlined if possible. The compiler may choose not to inline your function despite the presence of the keyword.

Likewise inlining functions is an optimisation step that your compiler might choose to do even without the inline keyword.

Pros:

  • Can improve the performance of your program by avoiding the various overhead of function calls

  • It increases locality of reference by utilizing instruction cache

Cons:

  • Can potentially can code bloat if the inlined function is not small, increasing the executable size

When to use:

  • When the function is simple & small

  • When the function is called in a tight loop

Function Pointers

Function pointers are a way to assign a function to a variable.

At the moment we only use functions as a symbol that we call or invoke; function pointers allow us to perform some logic around the function.

void SayHelloWorld() {
    std::cout << "Hello world!" << std::endl;
}

int main() {
    auto function = SayHelloWorld;
    function();  // This is the same as `SayHelloWorld()`
}
  • The auto keyword is useful for creating function pointers.

  • The actual type of a function is <return_type>(*<variable_name>)(<parameter_type>, <parameter_type>, ...)

  • The declaration void(*hello_function)() defines a function variable:

    • called hello_function

    • returns a void type

    • and takes no parameters

Lambda Functions

Lambda functions give us a way to define a function without actually creating a function.

The syntax for a lambda function is as follows:

[ <captures> ] ( <params> ) { <body> }
  • <captures> : refer to variables that exist within the scope where the lambda is defined

  • <params> : refer to any parameters that the lambda function accepts

  • <body> : refers to the body of the lambda function

Last updated

Was this helpful?