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 this pointer.
class MyClass {
public:
    static void staticMethod();
};

MyClass::staticMethod(); // Call the static method

constexpr

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: inline suggests that the compiler should inline the function, replacing the function call with the actual code of the function.
  • virtual: virtual is 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 C has a final method my_func, so class D cannot 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.
  • 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 arguments

Implement 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_list is initialized by va_start, and cleaned up by va_end.
    • Think about a pointer to a list of arguments, va_list type traverses the list.
  • va_arg(va_list ap, type) is used to get the next argument.
    • ap is the va_list variable initialized by va_start.
    • type is 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 args and allows you to perform further processing on the arguments contained in the parameter pack.

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 const object.

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 a const char* argument.
    • The operator() is the method name.
    • The second () takes the argument.

Footnotes

  1. Neither of them is a keyword, they are identifiers. Identifiers that have special meanings to the compiler at specific places. This means you can use them as variable names, but you should avoid doing so.↩︎

  2. You can mark all the functions in this page to be constexpr↩︎