Function Pointers
Function Objects: Functors
Lambda Expressions
Error Handling
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
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
#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;
}
#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;
}
#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;
}
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
#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 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 }
#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;
}
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]
#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;
}
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
#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;
}
Error handling is crucial in many areas such as:
→ Dynamic Memory allocation
→ File stream I/O operations
→ Networking
→ Preventing out of bounds issues
#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;
}
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
#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;
}