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