Skip to content

9. Functions

What & why

A function is a named, reusable block of code with inputs (parameters) and an output (return value). Functions let you break a big problem into small, testable pieces, and give each piece a name that explains what it does.

Defining and calling

#include <iostream>

// return_type name(parameters) { body }
int square(int x) {
    return x * x;
}

void greet(const std::string& name) {     // void = returns nothing
    std::cout << "Hello, " << name << "!\n";
}

int main() {
    std::cout << square(7) << '\n';   // 49
    greet("Ada");
}
  • Every non-void function must return a value on every path. Falling off the end is undefined behavior (-Wall warns you).
  • A void function can use a bare return; to exit early.

Declaration before use

The compiler reads top to bottom. You can't call a function before it's been declared. Either define it above main, or write a declaration (prototype) first:

int square(int x);           // declaration: "this exists"

int main() {
    std::cout << square(7);  // OK
}

int square(int x) {          // definition: "here's how it works"
    return x * x;
}

This separation is the foundation of header files (next lesson).

How arguments are passed

This is the most important section in this lesson.

By value — the function gets a copy

void try_to_change(int n) {
    n = 100;                 // changes the local copy only
}

int x = 5;
try_to_change(x);
// x is still 5

By reference — the function works on the caller's variable

void change(int& n) {
    n = 100;
}

int x = 5;
change(x);
// x is now 100

By const reference — no copy, no modification

double average(const std::vector<double>& values) {
    double sum = 0;
    for (double v : values) sum += v;
    return values.empty() ? 0.0 : sum / values.size();
}

Which one?

Situation Parameter type
Small, cheap types (int, double, char, bool) by value: int x
Large types you only read (std::string, std::vector, your classes) const T&
Read-only text std::string_view
You need to modify the caller's object T&

Prefer returning results over modifying reference parameters — it's clearer:

std::vector<int> evens_only(const std::vector<int>& v);   // good
void evens_only(const std::vector<int>& v, std::vector<int>& out);   // older style

Returning a local vector is cheap: the compiler moves it or constructs it in place.

Returning multiple values

Return a struct (or std::pair / std::tuple) and unpack with structured bindings:

struct MinMax {
    int min;
    int max;
};

MinMax min_max(const std::vector<int>& v) {
    auto [lo, hi] = std::ranges::minmax_element(v);
    return {*lo, *hi};
}

auto [smallest, largest] = min_max({4, 9, 1, 7});

Default arguments

void log(const std::string& msg, const std::string& level = "INFO") {
    std::cout << "[" << level << "] " << msg << '\n';
}

log("started");              // [INFO] started
log("disk full", "ERROR");   // [ERROR] disk full

Defaults must be the trailing parameters, and go in the declaration only (not repeated in the definition).

Overloading

Several functions can share a name if their parameter lists differ:

int area(int side) { return side * side; }
int area(int w, int h) { return w * h; }
double area(double radius) { return 3.14159 * radius * radius; }

area(4);      // 16
area(3, 5);   // 15
area(2.0);    // 12.566...

The compiler picks the best match. Overloads that differ only in return type are not allowed.

Recursion

A function that calls itself, with a base case that stops:

long long factorial(int n) {
    if (n <= 1) {
        return 1;                 // base case
    }
    return n * factorial(n - 1);  // recursive case
}

Each call uses stack space; very deep recursion (hundreds of thousands of calls) overflows the stack and crashes. Loops are usually safer for simple repetition.

[[nodiscard]]

Mark functions whose result should never be ignored:

[[nodiscard]] bool save(const std::string& path);

save("data.txt");   // warning: ignoring return value

Lambdas: tiny inline functions

You saw these with std::count_if. A lambda is a function you can write inline and store in a variable:

auto is_even = [](int x) { return x % 2 == 0; };
is_even(4);   // true

int threshold = 10;
auto above = [threshold](int x) { return x > threshold; };   // captures threshold

Full treatment in the intermediate track.

Small, well-named functions

Good functions:

  • do one thing, described by their name (parse_date, is_valid_email),
  • are short enough to read without scrolling,
  • avoid hidden side effects (global variables, printing inside a calculation),
  • take what they need as parameters and return their result.

Common mistakes

  • Passing big objects by value — silently copies a whole vector on every call.
  • Returning a reference to a local variable — the local dies when the function returns:
    const std::string& bad() { std::string s = "hi"; return s; }   // dangling!
    
    Return by value instead.
  • Missing return on some path of a non-void function.
  • Calling a function before declaring it.
  • Recursion without a base case — stack overflow.
  • Ambiguous overloads — f(int) and f(double) called with a long.

Exercises

  1. Write bool is_prime(int n) and use it to print the primes below 100.
  2. Write void swap_values(int& a, int& b) and prove it works. Then try it with int a, int b and explain why it doesn't.
  3. Write std::string repeat(std::string_view s, int times = 2).
  4. Write double average(const std::vector<double>&), double median(std::vector<double>) (why by value here?), and double stddev(const std::vector<double>&).
  5. Write a recursive int sum_digits(int n) and a recursive bool is_palindrome(std::string_view s).
  6. Refactor your number-guessing game from lesson 6 into functions: int read_int(std::string_view prompt), int pick_secret(), void play().

Recap

  • return_type name(params) { ... }; declare before use.
  • Pass small types by value, large read-only types by const&, text by std::string_view, and use & only when you must modify.
  • Return results (including structs for multiple values) rather than using out-parameters.
  • Default arguments and overloading make APIs convenient.
  • Never return a reference or pointer to a local.
  • Keep functions small and single-purpose.

Next: Headers, namespaces & multi-file programs →