Phase 1 · Modern C++ FoundationsModule 6~52 min read

Functions, Scope, References & Program Structure

Organize programs with functions, value and reference parameters, overloads, scope, storage duration, recursion, and clean interfaces.

What you'll learn

Functions turn calculations and behaviors into named contracts. C++ adds references, overloads, defaults, and generic functions so interfaces can express performance and mutation without giving up type safety.

By the end, you'll be able to:

  • Separate a function declaration from its definition
  • Choose between value, reference, and const-reference parameters
  • Use return values, overloads, defaults, and simple generic functions
  • Reason about scope, storage duration, stack frames, and recursion

Declarations & definitions

A declaration introduces a function's name and type. A definition supplies its body. A call must see a declaration first so the compiler can check argument and return types.

functions.cpp
#include <iostream>

int square(int value); // declaration: callable interface

int main() {
    int answer{square(7)}; // call
    std::cout << answer << '\n';
}

int square(int value) { // definition: implementation
    return value * value;
}
PartExampleContract role
Return typeintType produced for the caller
NamesquareBehavior's identifier
Parameters(int value)Inputs and their passing modes
Body{ return value * value; }Implementation in the definition

Pass by value

A value parameter is a new local object initialized from the argument. Modifying it does not modify the caller's object. This is an excellent default for small values and for inputs a function needs to own or transform independently.

pass_by_value.cpp
#include <iostream>

void add_bonus(int score) {
    score += 10; // modifies the function's local copy
    std::cout << "inside: " << score << '\n';
}

int main() {
    int score{80};
    add_bonus(score);
    std::cout << "caller: " << score << '\n';
}

Key idea

C++ is always explicit about the parameter declaration. int score copies a value;int& score binds a reference to the caller's object.

Reference parameters

An lvalue reference is another name for an existing object. A non-const reference makes caller-visible mutation part of the interface. A const reference avoids copying while promising not to modify through that parameter.

references.cpp
#include <iostream>
#include <string>

void swap_values(int& left, int& right) {
    int temporary{left};
    left = right;
    right = temporary;
}

void print_label(const std::string& label) {
    std::cout << label << '\n'; // read without copying
}

int main() {
    int first{10};
    int second{20};
    swap_values(first, second);
    print_label("after swap");
    std::cout << first << ' ' << second << '\n';
}
ParameterMeaningTypical use
T valueIndependent local valueSmall input or owned working copy
T& valueMutable aliasRequired caller-visible modification
const T& valueRead-only aliasRead a potentially expensive object without copying

Watch out

A reference must bind to a valid object. Never return a reference to a local automatic object; that object's lifetime ends when the function returns, leaving a dangling reference.

Designing return values

Prefer returning a result over mutating an output parameter when one clear result exists. Modern C++ efficiently returns standard-library and user-defined values through copy elision and move semantics.

return_value.cpp
#include <cctype>
#include <iostream>
#include <string>

std::string uppercase(std::string text) {
    for (char& character : text) {
        unsigned char safe{static_cast<unsigned char>(character)};
        character = static_cast<char>(std::toupper(safe));
    }
    return text; // returned by value; move/elision makes this efficient
}

int main() {
    std::string original{"Modern C++"};
    std::string result{uppercase(original)};
    std::cout << original << " -> " << result << '\n';
}
  • Return a value for a computed result
  • Use void when the function's purpose is an observable action
  • Use a non-const reference when mutation is central and obvious
  • Later, group several meaningful results in a class, struct, tuple, or result type

Defaults & overloading

Overloaded functions share a name but have distinguishable parameter lists. Default arguments let callers omit trailing arguments. Both should represent one coherent concept, not unrelated behaviors hidden behind the same spelling.

overloads.cpp
#include <iostream>

double area(double side) {
    return side * side;
}

double area(double width, double height) { // overload
    return width * height;
}

void print_area(double value, int precision = 2) { // default argument
    std::cout.precision(precision);
    std::cout << std::fixed << value << '\n';
}

int main() {
    print_area(area(4.0));
    print_area(area(4.0, 2.5), 1);
}

Note

The compiler chooses an overload from argument types and available conversions; return type alone cannot distinguish overloads. Put a default argument in one visible declaration, not repeatedly across declarations and definitions.

Generic functions

C++20 permits auto in a function parameter as an abbreviated function template. The compiler generates a suitable typed function for each supported argument combination.

generic.cpp
#include <iostream>
#include <string>

auto larger(auto left, auto right) {
    return left < right ? right : left;
}

int main() {
    std::cout << larger(4, 9) << '\n';
    std::cout << larger(2.5, 1.5) << '\n';
    std::cout << larger(std::string{"Ada"}, std::string{"Bjarne"}) << '\n';
}

Tip

Generic does not mean “accept everything.” This function requires values that can be compared and produce one compatible result. Phase 3 introduces concepts for expressing those semantic requirements directly.

Scope & namespaces

Scope determines where a name can be used. Keep names in the smallest useful scope so state and dependencies stay local. A namespace groups related declarations and avoids collisions.

scope.cpp
#include <iostream>

namespace {
constexpr int default_limit{10}; // visible only in this source file
}

int main() {
    int count{3};

    if (count < default_limit) {
        int remaining{default_limit - count};
        std::cout << remaining << '\n';
    }

    // remaining is not in scope here.
}
ScopeBeginsEnds
BlockAt a declaration inside bracesAt the matching closing brace
Function parameterIn the parameter declarationAt the function body's end
NamespaceAt a namespace declarationAcross matching namespace declarations
Unnamed namespaceAt its declarationSource-file translation unit; internal linkage

Storage & the call stack

Most local objects have automatic storage duration: construction occurs when control reaches the declaration and destruction occurs when the scope exits. A static local is initialized once and lives until program termination while keeping its name local to the function.

static_local.cpp
#include <iostream>

int next_id() {
    static int last_id{1000}; // initialized once, persists between calls
    return ++last_id;
}

int main() {
    std::cout << next_id() << ' '
              << next_id() << ' '
              << next_id() << '\n';
}

Watch out

Persistent mutable state creates coupling and later requires synchronization when accessed concurrently. Use a static local only when that shared lifetime is part of the design, not as a shortcut for passing state.

Recursion

A recursive function calls itself on a smaller problem. It needs a reachable base case and a progress rule. Each active call has its own parameters and local objects in a call frame.

factorial.cpp
#include <iostream>

unsigned long long factorial(unsigned int value) {
    if (value <= 1U) return 1ULL; // base case
    return value * factorial(value - 1U); // smaller problem
}

int main() {
    std::cout << factorial(5) << '\n';
}
Recursive calls create separate active frames
factorial(1)returns 1
factorial(2)waits for 1
factorial(3)waits for 2
main()waits for 6
new calls are pushed toward the top

Watch out

Factorial grows very quickly; the return type overflows for modest inputs. Recursion also consumes stack space. Validate the domain and prefer iteration when recursion adds no clarity.

Program structure

As a project grows, headers publish declarations and source files provide definitions. The preprocessor includes the header into each translation unit, while the linker connects calls to one definition.

temperature.h + temperature.cpp
// include/temperature.h — declarations
#ifndef TEMPERATURE_H
#define TEMPERATURE_H

double celsius_to_fahrenheit(double celsius);
bool is_valid_celsius(double celsius);

#endif

// src/temperature.cpp — definitions
#include "temperature.h"

double celsius_to_fahrenheit(double celsius) {
    return celsius * 9.0 / 5.0 + 32.0;
}

bool is_valid_celsius(double celsius) {
    return celsius >= -273.15;
}
  • Give each function one clear responsibility
  • Name functions with verbs that describe observable behavior
  • Keep interfaces smaller and more stable than their implementations
  • Use include guards so a header's declarations appear only once per translation unit
  • Validate preconditions at a clear system boundary

Key idea

A function signature is a contract: parameter modes express copying or aliasing, const expresses mutation policy, and the return type expresses the primary result.

Recap & quick check

Key takeaways

  • Declarations introduce callable interfaces; definitions provide implementations.
  • Value parameters copy, mutable references alias caller state, and const references read without copying.
  • Returning a value is the clearest design for one computed result and is efficient in Modern C++.
  • Overloads and defaults should represent one coherent operation.
  • Small scopes, explicit storage duration, and focused functions keep dependencies understandable.
  • Recursion needs a reachable base case, progress, and awareness of stack and numeric limits.

Quick check

1. Which parameter can modify the caller's int directly?

2. Why use const std::string& for a read-only large string parameter?

3. Can return type alone distinguish two overloads?

4. What two properties make recursion terminate correctly?

Phase 1 complete. Phase 2 begins with Module 7 — Arrays, std::array, std::vector & std::span, where functions start working with rich collections and explicit views.