Chapter 04

Functions

Organizing code into reusable, modular blocks for cleaner and more maintainable programs

What You Will Learn

  • How to define and call functions
  • Parameters, arguments, and return values
  • Pass by value, reference, and pointer
  • Default and optional parameters
  • Function overloading
  • Inline functions and their optimization
  • Recursive functions and their applications
  • Function pointers and callbacks
  • Lambda expressions for modern C++
  • constexpr functions for compile-time computation
  • Introduction to function templates

What Are Functions?

Functions are reusable blocks of code that perform specific tasks. They help:

  • Organize code - Break complex problems into smaller pieces
  • Reduce repetition - Write once, use many times (DRY principle)
  • Improve readability - Give meaningful names to operations
  • Enable testing - Test individual parts in isolation
  • Facilitate maintenance - Change behavior in one place
// Without functions - repetitive
int a = 5, b = 3;
int sum1 = a + b;
int c = 10, d = 7;
int sum2 = c + d;

// With functions - reusable
int add(int x, int y) {
    return x + y;
}
int sum1 = add(5, 3);
int sum2 = add(10, 7);

Function Syntax

Function Declaration (Prototype)

A declaration tells the compiler about a function's existence:

// Declaration (prototype) - no body
return_type function_name(parameter_list);

// Examples
int add(int a, int b);
void printMessage(std::string msg);
double calculateArea(double radius);

Function Definition

A definition includes the actual code:

// Definition - includes body
return_type function_name(parameter_list) {
    // function body
    return value;  // if non-void
}

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

Declaration vs Definition

// In header file (math_utils.h)
int add(int a, int b);  // Declaration

// In source file (math_utils.cpp)
int add(int a, int b) {  // Definition
    return a + b;
}

// In main.cpp
#include "math_utils.h"
int main() {
    int result = add(5, 3);  // Function call
}

Void Functions

Functions that don't return a value use void:

void greet(std::string name) {
    std::cout << "Hello, " << name << "!\n";
    // No return statement needed (or use: return;)
}

greet("Alice");  // Output: Hello, Alice!

Parameters and Arguments

Parameters are variables in the function declaration. Arguments are actual values passed when calling.

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

//   arguments
//   vvvvvv
add(5, 3);

Multiple Parameters

double calculateBMI(double weight, double height) {
    return weight / (height * height);
}

void printPerson(std::string name, int age, std::string city) {
    std::cout << name << ", " << age << ", from " << city << "\n";
}

No Parameters

int getRandomNumber() {
    return 42;  // Chosen by fair dice roll
}

void sayHello() {
    std::cout << "Hello!\n";
}

// Call with empty parentheses
int num = getRandomNumber();
sayHello();

Return Values

Functions can return a single value of any type.

Basic Return

int square(int x) {
    return x * x;
}

std::string getGreeting(std::string name) {
    return "Hello, " + name + "!";
}

bool isEven(int n) {
    return n % 2 == 0;
}

Early Return

int divide(int a, int b) {
    if (b == 0) {
        std::cout << "Error: Division by zero\n";
        return 0;  // Early return
    }
    return a / b;
}

bool isPositive(int n) {
    if (n <= 0) return false;
    return true;
}

Returning Multiple Values

C++ has several ways to return multiple values:

// Method 1: Using std::pair
std::pair<int, int> minMax(int a, int b) {
    if (a < b) return {a, b};
    return {b, a};
}
auto [min, max] = minMax(5, 3);  // Structured binding (C++17)

// Method 2: Using std::tuple
std::tuple<int, int, int> getStats(const std::vector<int>& v) {
    int sum = 0, min = INT_MAX, max = INT_MIN;
    for (int x : v) {
        sum += x;
        min = std::min(min, x);
        max = std::max(max, x);
    }
    return {sum, min, max};
}
auto [sum, minVal, maxVal] = getStats({1, 2, 3, 4, 5});

// Method 3: Using struct
struct Result {
    bool success;
    std::string message;
    int value;
};
Result compute() {
    return {true, "OK", 42};
}

// Method 4: Output parameters (less modern)
void getMinMax(int a, int b, int& min, int& max) {
    min = (a < b) ? a : b;
    max = (a > b) ? a : b;
}

Auto Return Type (C++14)

auto add(int a, int b) {
    return a + b;  // Compiler deduces return type as int
}

// Trailing return type (C++11)
auto add(int a, int b) -> int {
    return a + b;
}

Pass by Value vs Reference vs Pointer

Pass by Value (Copy)

A copy of the argument is made. Changes don't affect the original.

void doubleValue(int x) {
    x = x * 2;  // Only modifies the copy
}

int num = 5;
doubleValue(num);
std::cout << num;  // Still 5!

Pass by Reference

The function receives the original variable. Changes affect the original.

void doubleValue(int& x) {
    x = x * 2;  // Modifies original
}

int num = 5;
doubleValue(num);
std::cout << num;  // Now 10!

Pass by Const Reference

Efficient (no copy) but read-only. Best for large objects.

void printVector(const std::vector<int>& v) {
    for (int x : v) {
        std::cout << x << " ";
    }
    // v.push_back(1);  // Error! Can't modify const
}

// Use for:
// - Large objects (strings, vectors, classes)
// - When you don't need to modify the parameter

Pass by Pointer

void doubleValue(int* x) {
    *x = *x * 2;  // Dereference to modify
}

int num = 5;
doubleValue(&num);  // Pass address
std::cout << num;   // Now 10!

// Pointers can be null
void process(int* ptr) {
    if (ptr == nullptr) {
        std::cout << "Null pointer!\n";
        return;
    }
    // Safe to use *ptr
}

When to Use Each

Method Use When Example Types
Value Small types, need a copy int, double, char, bool
Reference (&) Need to modify original Any modifiable type
Const Reference (const &) Large objects, read-only string, vector, classes
Pointer (*) Optional parameter (nullable) Arrays, optional objects

Default Parameters

Functions can have default values for parameters.

void greet(std::string name = "World") {
    std::cout << "Hello, " << name << "!\n";
}

greet("Alice");  // Hello, Alice!
greet();         // Hello, World!

Multiple Default Parameters

void printInfo(std::string name, int age = 0, std::string city = "Unknown") {
    std::cout << name << ", " << age << ", " << city << "\n";
}

printInfo("Alice", 30, "NYC");  // Alice, 30, NYC
printInfo("Bob", 25);           // Bob, 25, Unknown
printInfo("Charlie");           // Charlie, 0, Unknown

Rules for Default Parameters

// Default parameters must be rightmost
void func(int a, int b = 10, int c = 20);  // OK
// void func(int a = 5, int b, int c);     // Error!

// Defaults in declaration (header), not definition
// In header:
void greet(std::string name = "World");
// In source:
void greet(std::string name) {  // No default here
    std::cout << "Hello, " << name << "!\n";
}

Function Overloading

Multiple functions can share the same name if they have different parameter lists.

// Same name, different parameters
int add(int a, int b) {
    return a + b;
}

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

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

// Compiler chooses based on arguments
add(1, 2);       // Calls int version
add(1.5, 2.5);   // Calls double version
add(1, 2, 3);    // Calls three-parameter version

Overload Resolution

void print(int x)    { std::cout << "int: " << x << "\n"; }
void print(double x) { std::cout << "double: " << x << "\n"; }
void print(std::string x) { std::cout << "string: " << x << "\n"; }

print(42);       // int: 42
print(3.14);     // double: 3.14
print("hello");  // string: hello
print('a');      // int: 97 (char promoted to int)

Overloading Rules

  • Functions must differ in parameter types or count
  • Return type alone doesn't distinguish overloads
  • const and non-const references count as different
// Error: Can't overload by return type only
int getValue();
// double getValue();  // Error!

// OK: Different const-ness
void process(int& x);
void process(const int& x);  // Different overload

Avoid Ambiguity

void func(int a, double b);
void func(double a, int b);
// func(1, 1);  // Error: ambiguous!

Inline Functions

The inline keyword suggests the compiler replace function calls with the function body.

inline int square(int x) {
    return x * x;
}

// When compiled, this:
int result = square(5);
// Might become:
int result = 5 * 5;

Benefits

  • Eliminates function call overhead
  • Enables further optimizations
  • Useful for small, frequently-called functions

Modern C++ and Inline

// constexpr functions are implicitly inline
constexpr int square(int x) { return x * x; }

// Functions defined in class body are implicitly inline
class Calculator {
public:
    int add(int a, int b) { return a + b; }  // Implicitly inline
};

// Modern compilers often inline automatically
// The 'inline' keyword is now mainly about linkage

Tip

Don't overuse inline. Modern compilers are smart about inlining. Use it primarily to allow function definitions in headers without ODR (One Definition Rule) violations.

Recursion

A recursive function calls itself. Every recursive function needs a base case to stop.

Factorial Example

int factorial(int n) {
    // Base case
    if (n <= 1) {
        return 1;
    }
    // Recursive case
    return n * factorial(n - 1);
}

// factorial(5) = 5 * factorial(4)
//              = 5 * 4 * factorial(3)
//              = 5 * 4 * 3 * factorial(2)
//              = 5 * 4 * 3 * 2 * factorial(1)
//              = 5 * 4 * 3 * 2 * 1
//              = 120

Fibonacci

// Simple but inefficient (exponential time)
int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}

// Efficient with memoization
std::unordered_map<int, long long> memo;
long long fibMemo(int n) {
    if (n <= 1) return n;
    if (memo.count(n)) return memo[n];
    memo[n] = fibMemo(n - 1) + fibMemo(n - 2);
    return memo[n];
}

Tail Recursion

// Tail recursive - the recursive call is the last operation
int factorialTail(int n, int accumulator = 1) {
    if (n <= 1) return accumulator;
    return factorialTail(n - 1, n * accumulator);
}

// Some compilers optimize tail recursion into a loop

When to Use Recursion

  • Tree/graph traversal
  • Divide and conquer algorithms
  • Problems with natural recursive structure
  • When the recursive solution is clearer

Stack Overflow

Deep recursion can cause stack overflow. Each call uses stack space. For deep recursion, consider iteration or tail recursion optimization.

Function Pointers

Functions can be stored in variables and passed as arguments.

Basic Syntax

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

// Function pointer declaration
int (*operation)(int, int);

// Assign and call
operation = add;
std::cout << operation(5, 3);  // 8

operation = subtract;
std::cout << operation(5, 3);  // 2

Using Type Aliases

// typedef (old style)
typedef int (*BinaryOp)(int, int);

// using (modern, preferred)
using BinaryOp = int (*)(int, int);

BinaryOp op = add;
std::cout << op(10, 5);  // 15

Callback Functions

void forEach(int arr[], int size, void (*callback)(int)) {
    for (int i = 0; i < size; i++) {
        callback(arr[i]);
    }
}

void printDouble(int x) {
    std::cout << x * 2 << " ";
}

int arr[] = {1, 2, 3, 4, 5};
forEach(arr, 5, printDouble);  // 2 4 6 8 10

std::function (Modern C++)

#include <functional>

std::function<int(int, int)> operation;

operation = add;
operation = [](int a, int b) { return a * b; };  // Lambda

// More flexible than raw function pointers
// Can hold functions, lambdas, and functors

Lambda Expressions (C++11)

Lambdas are anonymous functions defined inline.

Basic Syntax

// [capture](parameters) -> return_type { body }

auto add = [](int a, int b) {
    return a + b;
};

std::cout << add(3, 4);  // 7

Capture Clause

int multiplier = 10;

// Capture by value (copy)
auto mult = [multiplier](int x) {
    return x * multiplier;
};

// Capture by reference
auto increment = [&multiplier]() {
    multiplier++;  // Modifies original
};

// Capture all by value
auto f1 = [=]() { return multiplier; };

// Capture all by reference
auto f2 = [&]() { multiplier++; };

// Mixed captures
int a = 1, b = 2;
auto f3 = [a, &b]() { b = a; };  // a by value, b by reference

With STL Algorithms

#include <algorithm>
#include <vector>

std::vector<int> nums = {5, 2, 8, 1, 9};

// Sort descending
std::sort(nums.begin(), nums.end(), [](int a, int b) {
    return a > b;
});

// Find first greater than 4
auto it = std::find_if(nums.begin(), nums.end(), [](int x) {
    return x > 4;
});

// Count even numbers
int count = std::count_if(nums.begin(), nums.end(), [](int x) {
    return x % 2 == 0;
});

// Transform
std::vector<int> doubled;
std::transform(nums.begin(), nums.end(), std::back_inserter(doubled),
    [](int x) { return x * 2; });

Generic Lambdas (C++14)

// auto parameters
auto add = [](auto a, auto b) {
    return a + b;
};

std::cout << add(1, 2);      // 3
std::cout << add(1.5, 2.5);  // 4.0
std::cout << add(std::string("Hello"), std::string(" World"));  // Hello World

Mutable Lambdas

int count = 0;
// By default, captured-by-value variables are const
auto counter = [count]() mutable {
    return ++count;  // OK with mutable
};

std::cout << counter();  // 1
std::cout << counter();  // 2
std::cout << count;      // Still 0 (original unchanged)

Immediately Invoked Lambda

// IIFE - Immediately Invoked Function Expression
const int result = []() {
    // Complex initialization
    int x = heavyComputation();
    return x * 2;
}();  // Note the () at the end

Constexpr Functions (C++11/14/17)

constexpr functions can be evaluated at compile time.

constexpr int factorial(int n) {
    return (n <= 1) ? 1 : n * factorial(n - 1);
}

// Computed at compile time
constexpr int fact5 = factorial(5);  // 120

// Can also be used at runtime
int x;
std::cin >> x;
int result = factorial(x);  // Computed at runtime

constexpr vs const

const int a = 5;            // Constant, might be runtime
constexpr int b = 5;        // Guaranteed compile-time

const int c = getInput();   // OK: runtime constant
// constexpr int d = getInput();  // Error! Must be compile-time

C++14 Relaxed constexpr

// C++14 allows loops and local variables
constexpr int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

consteval (C++20)

// Must be evaluated at compile time (immediate function)
consteval int compiletimeOnly(int n) {
    return n * n;
}

constexpr int a = compiletimeOnly(5);  // OK
// int b = compiletimeOnly(x);  // Error if x is runtime

Function Templates (Introduction)

Templates allow writing generic functions that work with any type.

template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

// Compiler generates specific versions
std::cout << max(3, 7);        // max<int>
std::cout << max(3.5, 7.2);    // max<double>
std::cout << max('a', 'z');    // max<char>

Multiple Template Parameters

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

auto result = add(5, 3.14);  // int + double = double

Template Specialization

// General template
template <typename T>
void print(T value) {
    std::cout << value << "\n";
}

// Specialization for bool
template <>
void print<bool>(bool value) {
    std::cout << (value ? "true" : "false") << "\n";
}

print(42);      // 42
print(true);    // true (not 1)

Note

Templates are covered in more depth in Chapter 10: Advanced Topics.

Best Practices

  • Single Responsibility - Each function should do one thing well
  • Meaningful Names - Use verb phrases: calculateArea(), isValid()
  • Keep Functions Short - If it's too long, break it into smaller functions
  • Prefer const reference for parameters that don't need modification
  • Use nodiscard (C++17) for functions whose return value shouldn't be ignored
  • Document complex functions with comments explaining parameters and return values
// Good practices example
/**
 * Calculates the area of a rectangle.
 * @param width The width of the rectangle (must be positive)
 * @param height The height of the rectangle (must be positive)
 * @return The area, or -1 if dimensions are invalid
 */
[[nodiscard]] double calculateArea(double width, double height) {
    if (width <= 0 || height <= 0) {
        return -1;
    }
    return width * height;
}

Practice Questions

  1. Write a function that returns the maximum of three numbers.
  2. Create a function to check if a number is prime.
  3. Write a function to reverse a string.
  4. Implement a recursive function to calculate the nth Fibonacci number.
  5. Create overloaded functions to calculate the area of different shapes (circle, rectangle, triangle).
  6. Write a function that takes a callback and applies it to each element of an array.
  7. Create a lambda that captures a variable and counts how many times it's called.
  8. Write a constexpr function to calculate the sum of digits in a number.
  9. Implement a function template that swaps two values of any type.
  10. Create a function that returns multiple values using std::tuple.
  11. Write a function using default parameters to format a date string.
  12. Implement binary search as a recursive function.