Course outline

C++ / Basic / Lesson 4

Functions, references, and contracts

Express a calculation as a small interface, choose how arguments cross it, and prevent references from outliving the objects they identify.

About 4 min readContent reviewed
On this page

Lesson goals

  • Separate declarations, definitions, arguments, and return values.
  • Choose value, mutable reference, or const reference intentionally.
  • Document preconditions, postconditions, and borrowing lifetimes.

Give a function one understandable job

A function declaration describes a name, parameter types, and return type; its definition supplies the body. An argument initializes a parameter when the function is called. Prefer a small function whose name states the domain operation rather than the implementation steps. A contract states what callers must provide and what the function promises afterward. For a discount calculation, accepted prices and percentage limits matter as much as the arithmetic. Returning a value keeps the result visible and avoids surprising changes to distant shared state.

A value parameter is a separate object

Passing an int by value copies its value into a separate parameter. Assigning that parameter does not change the caller variable. This is usually the clearest choice for small ordinary values. A return value transfers the answer back explicitly. In the first example, doubled receives a copy of tickets, computes its result, and leaves tickets unchanged. A function returning an object by value is also often appropriate for larger results; modern C++ can construct results directly, so do not return a reference to a local object to avoid an imagined copy.

cpp Copy an input and return a result
#include <iostream>
int doubled(int value) { return value * 2; }
int main() {
    const int tickets{6};
    const int result = doubled(tickets);
    std::cout << tickets << " " << result << "\n";
}
Expected output
6 12

The parameter is independent of tickets. For this small input, multiplication is representable and the caller retains six.

A reference makes an existing object accessible

An int& parameter aliases an existing integer and permits modification. A const std::string& borrows a string for reading without copying it through that parameter. const restricts this access path; it does not create an owned snapshot. References must be initialized and cannot later be reseated. In the second example add_bonus visibly modifies its argument, while length_report only observes a live string. The name of a mutating function should make its effect clear, and the caller must keep the referred object alive throughout the call.

cpp Mutation and observation use different interfaces
#include <iostream>
#include <string>
void add_bonus(int& points) { points += 3; }
std::string::size_type length_report(const std::string& name) {
    return name.size();
}
int main() {
    int points{7};
    const std::string name{"Lina"};
    add_bonus(points);
    std::cout << points << " " << length_report(name) << "\n";
}
Expected output
10 4

The integer alias changes points to ten; the const reference reads four bytes without modifying or storing the string.

Borrowing does not extend every lifetime

A reference identifies an object rather than owning its lifetime. Returning a reference to a local variable leaves a dangling reference after the function returns; using it as a live object is undefined behavior. A temporary bound to a const reference parameter survives the call, not an arbitrary later use of a stored reference. Return values for newly created data, and reserve returned references for objects whose owner and lifetime are explicit. Draw the owner scope and every borrower when an interface becomes difficult to reason about.

Keep overloads predictable

Overloading gives multiple functions the same name with distinct parameter lists. Return type alone cannot distinguish overloads. Implicit conversions can select an unexpected candidate or make a call ambiguous, so avoid overload sets that demand subtle numeric guesses. Declare functions before calls or place definitions earlier in the file. Test the contract at ordinary values and boundaries, including whether the input changed. A pure calculation is easy to test because its inputs and outputs are explicit; a mutating operation additionally needs assertions about the resulting state.

Common errors and fixes

The caller value does not change.

Why it happens
The function modifies a value parameter copy.
How to fix it
Return the changed result or use an intentional mutable reference.
Prevent it next time
Choose parameter semantics before implementing the body.

A returned reference becomes invalid.

Why it happens
Its local owner was destroyed on function return.
How to fix it
Return the new object by value.
Prevent it next time
Document the owner for every escaping reference.

A call is ambiguous between overloads.

Why it happens
Several implicit conversions have comparable ranking.
How to fix it
Use a clearer parameter type or distinct operation names.
Prevent it next time
Keep overload families semantically consistent.

Check your understanding

1. Write the contract for a function that returns the larger of two int values without changing either input. Choose parameter and result forms.

Show worked solution

Use int larger(int left, int right) and return left >= right ? left : right. Both parameters are small copies and the result is a value, so no lifetime dependency escapes. Equal arguments return that equal value. Tests larger(3, 8), larger(8, 3), and larger(4, 4) yield 8, 8, and 4.

2. A helper builds a local std::string and returns const std::string&. Explain the defect and redesign it without giving the invalid program an output.

Show worked solution

The local string is destroyed before the caller can safely use the reference. Change the return type to std::string and return the string by value. The caller then owns its result independently. Copy elision or moving can make this efficient, but ownership correctness is the primary reason. Never test the dangling version by assuming its observed text is guaranteed.

Key takeaways

  • Pass small values by value and make mutation explicit.
  • Return owned results for newly created objects.
  • Every borrowed reference needs a valid owner lifetime.

Official references