Lesson overview
Functions let programmers package behaviour behind a name. Used well, they reduce duplication and make intent explicit. Used badly, they hide side effects, leak lifetime mistakes and make performance harder to reason about.
Learning objectives
- Explain why functions are used for reuse, abstraction and evidence of intent.
- Describe a function in terms of name, parameter list, return type and body.
- Choose suitable parameter passing styles for value, const reference, reference and ownership cases.
- Separate computation from input/output side effects.
- Refactor repeated code into focused functions that can be tested independently.
Key vocabulary before you start
Abstraction
A named boundary that hides detail while preserving a clear contract.
Parameter
A named input or output channel declared by a function.
Return value
The result produced by a function call.
Side effect
A change outside the returned value, such as mutation or output.
Contract
The expectations a caller and callee rely on.
Test seam
A boundary where behaviour can be tested independently.
Function design map
A good function has one clear reason to exist, a signature that communicates intent and a body that can be evaluated.
Name the operation
A function name should describe the work being done, not the mechanics of typing it.
Make the boundary explicit
Inputs, outputs, mutation and ownership should be visible in the signature.
Keep it testable
Prefer pure computations where possible and isolate I/O at the edge.
Return safe values
Never return references or pointers to dead local state.
Example: a small, testable function
The signature gives the caller a clear contract: two integers in, one integer out.
int add(int x, int y) {
return x + y;
}
int main() {
int total = add(2, 3);
return total == 5 ? 0 : 1;
}Trace the state
- The caller evaluates the argument expressions.
xandyreceive the argument values.- The function computes a result without modifying caller state.
- The result is returned to the call site.
| Line or operation | Variable | Value | State role | Invariant or check |
|---|---|---|---|---|
| Read the example before running it | Key names and calls | Known from source | Reasoning setup | Each named element should support the lesson focus: Functions in C++. |
| Trace the first meaningful operation | Primary state or boundary | Established by initialisation, call or condition | Valid-state checkpoint | Later reasoning must not use the value before this checkpoint. |
| Identify the first decision or dereference | Control or access point | Depends on current state | Failure-mode checkpoint | The condition, pointer, argument or dependency must be valid before use. |
| Record the observable result | Return value, output or mutation | Produced by the example | Evidence checkpoint | The result should match the contract explained in the lesson. |
Visual model: function contract boundary
A function is useful when its boundary makes data flow, responsibility and evidence clearer.
Name the responsibilityThe function name states one reason to call it.
Bind inputsParameters carry required data with explicit mutability and ownership.
Protect assumptionsPreconditions and validation prevent invalid calls where possible.
Produce evidenceReturn values, tests and diagnostics make behaviour checkable.
Limit side effectsOutput and mutation are deliberate, visible and justified.
Contract task
For one function in the mini exercise, write its responsibility, input contract, output contract and main failure mode.
Why use functions?
Functions reduce repetition, give behaviour a name and create boundaries that can be tested, reviewed and reused.
A function is also a design claim. It says this piece of work is coherent enough to deserve an interface.
At Masters level, the question is not whether code can be placed in a function. The question is whether the function boundary makes the program easier to reason about.
Example: give a task a name
Sorting is a distinct operation, so a function name can clarify intent.
void sort_numbers(std::vector<int>& nums) {
std::sort(nums.begin(), nums.end());
}Applied task
Choose a repeated code fragment and decide whether a function boundary would improve the program.
Features of a function
A C++ function has a return type, a name, a parameter list and a body. Together, these form a contract with the caller.
The parameter list is not just syntax. It describes what the function needs and whether caller state may be changed.
The return type should describe the result. If there is no meaningful result, void may be appropriate, but avoid using void to hide useful information from the caller.
Return values and side effects
Prefer returning a computed value when the caller needs the result.
double calculate_average(const std::vector<int>& scores) {
if (scores.empty()) return 0.0;
int total = std::accumulate(scores.begin(), scores.end(), 0);
return total / static_cast<double>(scores.size());
}
void print_message(const std::string& msg) {
std::cout << msg << '\n';
}Applied task
For each function in a small file, identify its inputs, output and side effects.
Functions, memory and performance
Function calls create parameter bindings and may copy values. For small scalar types, copying is usually fine. For larger objects, pass by const& when read-only access is enough.
Do not optimise blindly. First choose a signature that communicates intent; then measure if performance matters.
inline can remove call overhead in suitable cases, but it is a request to the compiler, not a design substitute.
Example: tiny inline function
For small functions, the compiler may inline the call.
inline int square(int x) {
return x * x;
}Example: parameter passing intent
The signature says the vector will be read but not modified.
int sum(const std::vector<int>& values) {
return std::accumulate(values.begin(), values.end(), 0);
}Applied task
Pick a function that takes a large object by value and decide whether const& would express intent better.
Guidelines for writing functions
Keep functions small enough to evaluate, but not so fragmented that the reader must chase trivial wrappers.
Separate computation from presentation. A function that both calculates an average and prints it is harder to test than one that returns the average and lets another function display it.
Document unusual ownership or lifetime expectations in the type system where possible.
Applied task
Rewrite one function so computation and output are separated.
Functions that return nothing
void is appropriate when the function's purpose is an effect: printing, logging, modifying a supplied object or sending a message.
It is not appropriate when the caller needs a result that could be returned safely.
Review void functions carefully: ask what state they change and how a caller can tell whether the operation succeeded.
Example: effect-only function
This function communicates that printing is the operation.
void print_report(double average) {
std::cout << "Average: " << average << '\n';
}Applied task
Identify one void function that should return a status or value instead.
Applied case lab
Case 1: The repeated report
Three repeated blocks compute and print averages. Extract the calculation first, then decide whether the printing deserves a separate function.
Case 2: The oversized parameter
A function copies a large vector but never modifies it. Decide whether to pass by value, const&, & or move.
Case 3: The unsafe return
A function returns a reference to a local max variable. Explain the lifetime bug and return a value instead.
Applied task: design, refactor and debug functions
Practise extracting functions, choosing parameter styles and making return values safe.
Stage 1: read and classify
A. Extract and generalise
This style repeats calculation and output. Split the responsibilities.
int a = 70, b = 80, c = 90;
double avg = (a + b + c) / 3.0;
std::cout << "Average: " << avg << "\n";- Create
compute_averagefor a collection of scores. - Create
print_reportfor output. - Explain which function is easier to unit test.
Reveal one possible refactor
double compute_average(const std::vector<int>& scores) {
if (scores.empty()) return 0.0;
int total = std::accumulate(scores.begin(), scores.end(), 0);
return total / static_cast<double>(scores.size());
}
void print_report(double average) {
std::cout << "Average: " << average << "\n";
}Model reasoning
A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.
Stage 2: trace or repair
B. Pick parameter passing
Design a signature for returning the top k values without surprising mutation.
std::vector<int> top_k_sorted(/* choose parameters */);- Decide whether the input vector should be copied, borrowed by
const&, or modified by&. - Explain the cost and mutation contract.
Reveal one possible refactor
std::vector<int> top_k_sorted(std::vector<int> values, std::size_t k) {
std::sort(values.begin(), values.end(), std::greater<int>{});
if (k < values.size()) values.resize(k);
return values;
}Model reasoning
A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.
Stage 3: justify the design
C. Fix lifetime
The reference returned here is invalid.
const int& largest_ref(const std::vector<int>& values) {
int max = values.front();
for (int v : values) if (v > max) max = v;
return max;
}- Explain why the returned reference dangles.
- Return a safe value or a reference to an element that still exists.
Reveal one possible refactor
int largest_value(const std::vector<int>& values) {
return *std::max_element(values.begin(), values.end());
}Model reasoning
A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.
Quick checks
1. Which signature communicates read-only access to a large vector?
2. Why separate calculation from printing?
3. What is unsafe about returning a reference to a local variable?
AI-augmented practice notes
Use these as prompts for disciplined support, not as permission to outsource judgement.
Function Review Coach
- Ask AI to list each function's inputs, outputs, side effects and ownership expectations.
- Ask for a refactor that separates calculation from I/O.
- Ask AI to suggest unit tests for each pure function.
Signature Designer
- Provide a candidate signature and ask whether value,
const&,&or move better communicates intent. - Ask AI to flag lifetime hazards in return types.
Assessment tasks
- Refactor one repeated code block into two focused functions.
- Write a short note explaining each parameter-passing choice in your refactor.
- Add two tests or test cases for the pure calculation function.
Judgement questions
What makes a function boundary worth having?
Use one example where a function improves meaning and one where it would add unnecessary indirection.
Which side effect should be isolated in your own code?
Pick printing, logging, mutation or allocation and explain how a better function boundary would reveal it.
