SEP200

Function Pointers, Lambda Expressions, and Error Handling

Summary

Function Pointers

Function Objects: Functors

Lambda Expressions

Error Handling

Function Pointers

Introduction

Like variables, functions are also stored in memory

As such, their addresses can also be accessed via pointers

This address identifies the location in memory where control is transferred to the function

Syntax and Usage

The syntax to create a function pointer is:

return-type (*pointer_name)(parameters_types) = function_name

Once created, a function pointer can be used to:

  call a function indirectly

  be passed as an argument to another function (callback)

  implement function tables

Function Pointers

Example of indirect call

#include <iostream>

void greet() {
    std::cout << "Hello, World!" << std::endl;
}

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

int main() {
    void (*greet_ptr)() = greet;
    greet_ptr();
    
    int (*add_ptr)(int, int) = add;
    std::cout << "Sum: " << add_ptr(3, 4) << std::endl;

    return 0;
}

              

Function Pointers

Example of templated callback

#include <iostream>

template <typename T>
T apply(T (*func)(T, T), T arg1, T arg2) {
    return func(arg1, arg2);
}

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

template <typename T>
T sub(T a, T b) {
    return a - b;
}

template <typename T>
T mult(T a, T b) {
    return a * b;
}

template <typename T>
T div(T a, T b) {
    return a / b;
}

int main() {

    int a = 12, b = 5;

    std::cout << "Sum: " << apply(add, a, b) 
              << std::endl;
    std::cout << "Subtraction: " << apply(sub, a, b) 
              << std::endl;
    std::cout << "Multiplication: " << apply(mult, a, b) 
              << std::endl;
    std::cout << "Division " << apply(div, a, b) 
              << std::endl;
              
    return 0;
}

              

Function Pointers

Example of a function table

#include <iostream>

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

template <typename T>
T sub(T a, T b) {
    return a - b;
}

template <typename T>
T mult(T a, T b) {
    return a * b;
}

template <typename T>
T div(T a, T b) {
    return a / b;
}

int main() {
    int (*operations[4])(int, int) = {add, sub, mult, div};

    int a = 10, b = 5;
    std::cout << "The results of the basic operations are:" 
              << std::endl;

    for (int i = 0; i < 4; i++){
        std::cout << operations[i](a, b) << std::endl;
    }    

    return 0;
}

              

Function Object

Introduction

A function object is a class that can act (be called) like a function

It is usually called a functor

As opposed to regular functions, functors can keep track of state

To create a functor, the operator () needs to be overloaded

Function Object

Example

#include <iostream>

enum operations {add, sub, mult, division};

class Arithmetics {
public:
    operations operation_ = add;

    Arithmetics(operations op) : operation_(op){};
    
    int operator()(int a, int b) {
        int result;
        if (operation_ == add) {
            result = a + b;
        } else if (operation_ == sub){
            result = a - b;
        } else if (operation_ == mult){
            result = a * b;
        } else {
            result = a / b;
        }        
        return result;
    }
};

int main() {
    
    Arithmetics arithmetics(add);

    std::cout << "Result: " << arithmetics(5, 3) 
              << std::endl;
    arithmetics.operation_ = sub;
    std::cout << "Result: " << arithmetics(5, 3) 
               << std::endl;
    arithmetics.operation_ = mult;
    std::cout << "Result: " << arithmetics(6, 3) 
               << std::endl;
    arithmetics.operation_ = division;
    std::cout << "Result: " << arithmetics(6, 3) 
               << std::endl;

    return 0;
}

              

Lambda Expressions

Introduction

Lambda expressions are anonymoys functions (functors) used for short snippets of code

They are not designed to be reused

They are often treated as inline functions

The syntax to create a lambda expression is:

[capture-list] (parameters) - > return_type { expression }

Lambda Expressions

Example

#include <iostream>

int main()
{
    
    auto hello = []() { return "Hello World"; };
    auto add4  = [](int i) { return i + 4; };

    std::cout << hello()  << std::endl;
    std::cout << add4(10) << std::endl;

    std::cout << []() { return "Straight hello World"; }() 
              << std::endl;
    std::cout << [](int i) { 
                    return i + 10; 
                 }(10) 
              << std::endl;

    return 0;

}

Capture

In the previous example, the lambda expressions did not capture non-local variables

You can choose to capture by value or by reference

You can also specify which variables to capture by value and which to capture by reference

Examples: [=], [&], [=x, &y], [=, &x, &y]

Lambda Expressions

Example

#include <iostream>

int main()
{

    int number = 4;
    std::string message = "Hello World";
    
    auto hello = [=]() { return message; };
    auto add  = [=](int i) { return i + number; };

    std::cout << hello()  << std::endl;
    std::cout << add(10) << std::endl;

    std::cout << [=]() { return "Straight " + message; }() 
              << std::endl;
    std::cout << [=](int i) { return i + number; }(10) 
              << std::endl;

    return 0;

}

Error Handling

Introduction

Object-oriented programs separate the source of errors from their handling

This is achieved via the following mechanism:

  1. a try statement is used

  2. if an exception happens, throw and error

  3. a catch statement will then handle this exception

Error Handling

Example

#include <iostream>
#include <string>

int safe_division(int dividend, int quotient) {
    if (quotient == 0) {
        throw "Division by 0 is undefined!";
    }
    return dividend / quotient;
}

int main() {
    int dividend = 10, quotient = 2;

    //std::cout << "Result: " << dividend/quotient 
    //          << std::endl;
  
    try {
        int result = safe_division(dividend, quotient);
        std::cout << "Result: " << result << std::endl;
    } 
    catch (const char* error) {
        std::cerr << "Error: " << error << std::endl;
    }
    
    return 0;
}

Usage

Error handling is crucial in many areas such as:

  Dynamic Memory allocation

  File stream I/O operations

  Networking

  Preventing out of bounds issues

Error Handling

Example

#include <iostream>

int print_element(int arr[], int i) {
    if (i > 4) {
        throw "Index out of bounds";
    }
    return arr[i];
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int index = 6;

    try {
        std::cout << "Element " << index << ": " 
                  << print_element(arr, index) 
                  << std::endl;
    }
    catch (const char *error) {
        std::cerr << "Error: " << error << std::endl;
    }

    return 0;
}

Exit and Abort

Introduction

Frequently, it makes sense to exit an application when an exception occurs

This can be accomplished using:

  abort() Terminates the application immediatly

  exit() Allows the C++ runtime termination process

Error Handling

Example

#include <iostream>
#include <string>

class Class {
private:
    std::string type_;
public:
    Class(std::string type) {
        type_ = type;
        std::cout << "Constructor called for " 
                  << type_ << std::endl;
    }

    ~Class() {
        std::cout << "Destructor called for " 
                  << type_ << std::endl;
    }
};

Class object("global");

int main() {
    Class local_object("local");
    std::cout << "Before calling exit() or abort()" 
              << std::endl;
    exit(EXIT_SUCCESS);
    //abort();

    std::cout << "After calling exit() or abort()" 
              << std::endl;
    return 0;
}

Suggested Reading

Function Pointers and Lambda Expressions

Error Handling