Function 3
Function modifiers
The function declaration has the following form:
prefix return_type func_name(parameter_list) suffix;You can provide a number of optional modifiers (or specifiers) to functions.
- Modifiers alter the behavior of the function.
- Some modifiers appear before the return type (
prefix), some appear at the end (suffix).
Prefix modifiers
static function
When a function is declared static at the global (file) scope, its visibility is limited to the file in which it is defined.
- It can be called only within the file and can’t be used in other files.
- Useful for helper functions that are only used within the file, and avoid name conflicts.
static class method
In the context of a class, the static modifier is used to define static member functions.
- These functions belong to the class rather than an instance of the class.
- Recall the static variable belongs to the class, not to an instance.
- As a result, they can be called using the class name (without creating an object) and don’t have access to
thispointer.
class MyClass {
public:
static void staticMethod();
};
MyClass::staticMethod(); // Call the static methodconstexpr
The constexpr keyword is to indicate that the function should be evaluated at compile time if possible.
- It can be used to optimize performance by avoiding runtime computation.
- The function must be a pure function
- It should not have side effects and should only depend on its input parameters.
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}Other prefix modifiers
inline:inlinesuggests that the compiler should inline the function, replacing the function call with the actual code of the function.virtual:virtualis used in class methods that can be overridden in derived classes.[[nodiscard]]: The function returns a value that should be used.[[noreturn]]: The function won’t return.
Suffix modifiers
override and final1
override: Indicates that the function is intended to override a virtual function from a base class.final: Prevents a function from being overridden in derived classes.- Another usage is to prevent a class from being inherited.
class A {
public:
virtual void my_func();
};
class B : public A {
public:
void my_func() override;
};
class C : public B {
public:
void my_func() final;
};
class D : public C {
public:
void my_func() override; // Error: my_func is final in C
};- class
Chas afinalmethodmy_func, so classDcannot override it.
Variadic functions/templates
Variadic functions
Variadic functions take a variable number of arguments.
- With variadic functions, you can take any number of arguments.
- If you are a python programmer, you can think of it as
*args.
- If you are a python programmer, you can think of it as
- The compiler matches the arguments to the parameters based on the order, any leftovers pack into the variadic parameter represented by
....
int sum(size_t n, ...);
// usage
int num1 = sum(1, 1);
int num2 = sum(2, 2, 2);
int num3 = sum(6, 2, 4, 6, 8, 10, 12); // 6 means there are 6 argumentsImplement a variadic function
You can’t extract elements from the variadic parameter directly.
- You access the individual element by using utility functions in
<cstdarg>.
| Function | Description |
|---|---|
va_list |
A type used to hold the information needed to retrieve the additional arguments after the fixed parameters. |
va_start |
Initializes a va_list variable to retrieve the additional arguments. |
va_arg |
Retrieves the next additional argument from the va_list. |
va_end |
Cleans up the va_list when done. |
va_copy |
Copies a va_list. |
int sum(size_t n, ...){
va_list args;
va_start(args, n); // initialize the list
int result = 0;
for (size_t i = 0; i < n; ++i) {
auto next_arg = va_arg(args, int);
result += next_arg;
}
va_end(args); // clear the list
return result;
}va_listis initialized byva_start, and cleaned up byva_end.- Think about a pointer to a list of arguments,
va_listtype traverses the list.
- Think about a pointer to a list of arguments,
va_arg(va_list ap, type)is used to get the next argument.apis theva_listvariable initialized byva_start.typeis the type of the next argument.- It returns the next argument and moves the pointer forward.
Variadic templates
There’re at least two disadvantages of variadic functions:
- Variadic parameters are not type-safe, you can’t check the type of the arguments.
- The number of arguments has to be known at compile time.
Variadic templates provides a safer and more flexible alternative. To declare a variadic template, you add a special template parameter ... called “parameter pack”.
Parameter pack
template <typename... Args>
return_type func_name(Args... args);Args...is the parameter pack, it’s part of the function parameter list.- You can invoke a function inside the function template with the parameter pack.
- Syntax:
func_name(args...). - This expands the parameter pack
argsand allows you to perform further processing on the arguments contained in the parameter pack.
- Syntax:
Programming with Parameter Packs
Unfortunately, the usage of parameter packs is not straightforward.
- You can’t directly access the elements of the parameter pack.
- A process called “compile-time recursion” is needed to process the parameter pack.
// base case
template <typename T>
void my_func(T x) {
// do something with x
}
template <typename T, typename... Args>
void my_func(T x, Args... args) {
// Use x, then recurse:
my_func(args...);
}Revisiting the sum function2
template <typename T>
T sum(T x) {
return x;
}
template <typename T, typename... Args>
T sum(T x, Args... args) {
return x + sum(args...);
}
// usage
int num1 = sum(1); // 1
int num2 = sum(2, 2); // 4
int num3 = sum(6, 2, 4, 6, 8, 10, 12); // 48- The base case
template <typename T>is needed to terminate the recursion. - The compiler will expand the parameter pack
Args...into a series of arguments until the base case is reached.
Fold expressions
C++17 introduces fold expressions, which provide a concise way to perform operations on a parameter pack.
When you want to perform operations on a parameter pack, you can use fold expressions. The syntax is as follows:
template <typename... Args>
return_type func_name(Args... args) {
return (args op ...); // op is the operator you want to apply
}For example, you can use fold expressions to implement the sum function.
template <typename... Args>
auto sum(Args... args) {
return (args + ...);
}A more complex example
Sometimes you need to perform non-binary operations on different elements of the parameter pack. In this case, you can only use compile-time recursion.
For example, you want to print a list of arguments with index.
// Base case
void print_with_index(size_t) {} // Do nothing
// Recursive case
template <typename T, typename... Args>
void print_with_index(size_t index, T first, Args... rest) {
std::cout << "Argument " << index << ": " << first << std::endl;
print_with_index(index + 1, rest...); // Recur with next index
}
int main() {
print_with_index(0, "Hello", 42, 3.14, 'A');
return 0;
}Function pointers
Function pointer
Function also occupy memory, so you can have a pointer to a function.
- Unlike other pointers, you can’t modify the pointed-to function.
- Think about it as a pointer to a
constobject.
- Think about it as a pointer to a
Declare a function pointer has a “ugly” syntax.
return_type (*pointer_name)(arg_type1, arg_type2, ...);It has a same syntax as a function declaration, but with a * in front of the name.
Function pointer as a parameter
Suppose you write two functions, one to add all the numbers in an array, and one to calculate the product of all the numbers in an array.
int sum_array(int* array, size_t size);
int product_array(int* array, size_t size);- Most of the codes of these two functions are the same, except for the operation.
- And to allow more future extension, you can pass a function pointer as a parameter.
int apply_operation(int* array, size_t size, int (*operation)(int, int));The implementation of apply_operation is straightforward.
int apply_operation(int* array, size_t size, int (*operation)(int, int)) {
int result = array[0];
for (size_t i = 1; i < size; ++i) {
result = operation(result, array[i]);
}
return result;
}
// functions
int sum(int a, int b) { return a + b; }
int product(int a, int b) { return a * b; }
// usage
int sum_result = apply_operation(array, size, sum);
int product_result = apply_operation(array, size, product);- When pass function pointer as a parameter, you don’t need to add
&to the function name, it’s a pointer already.
Function pointer combining with templates
In the previous example, you can only do integer operations. To allow more flexibility, you can combine function pointer with templates.
template <typename T>
T apply_operation(T* array, size_t size, T (*operation)(T, T)) {
T result = array[0];
for (size_t i = 1; i < size; ++i) {
result = operation(result, array[i]);
}
return result;
}
template <typename T>
T sum(T a, T b) { return a + b; }
template <typename T>
T product(T a, T b) { return a * b; }
// usage
double array[] = {1.1, 2.2, 3.3, 4.4, 5.5};
size_t size = 5;
double sum_result = apply_operation(array, size, sum);
double product_result = apply_operation(array, size, product);Function object (Functor)
What is a function object?
- A function object is an object that can be called like a function.
- It’s a class that overloads the
operator()(call operator). - It’s a kind of “function wrapper”.
In the previous assignment, you have already used function objects. For example, we define a class to calculate the payoff for a European call option.
class EuropeanCallPayoff {
public:
double operator()(double spot) const;
double strike;
};Counting the number of a certain element in an array
struct CountIf {
CountIf(char value) : value(value) {}
size_t operator()(const char* str) const {
size_t count = 0;
for (const char* p = str; *p != '\0'; ++p) {
if (*p == value) ++count;
}
return count;
}
private:
char value;
};- The
operator()is overloaded to take aconst char*argument.- The
operator()is the method name. - The second
()takes the argument.
- The