Chapter 07 of 24 | C++

Functions and program structure

A function gives a name to a reusable operation. It can accept parameters and return a result, helping you separate a problem into small parts with clear responsibilities.

double rectangle_area(double width, double height)
{
    return width * height;
}

int main()
{
    const double area = rectangle_area(4.0, 2.5);
    std::cout << area << '\n';
}

The function declaration specifies a return type, name, and parameter list. A function that does not return a value uses void. A call supplies arguments corresponding to its parameters. Each invocation has its own local variables, which normally cease to exist when the function returns.

Pass values intentionally

Passing a parameter by value makes a separate parameter object. This is straightforward for small types such as numbers. For larger objects, a const reference avoids copying while preventing the function from modifying the caller's object:

void print_name(const std::string& name)
{
    std::cout << name << '\n';
}

Use a non-const reference when changing the caller's object is an explicit part of the function's job. A pointer can represent an optional or reseatable relationship, but do not use it just to avoid a simple value parameter.

Declarations, overloads, and defaults

A declaration tells the compiler a function's interface; a definition provides its body. A function may be overloaded when its parameter lists differ, but changing only the return type does not create a valid overload. Default arguments can make common calls shorter, though too many defaults can obscure what a call means.

Keep a function focused. Its name should describe its action or result, and its parameters should be the information it needs. Prefer returning a result over silently changing unrelated global state.

Designing a useful function boundary

A function is most useful when its inputs and outputs are visible from its signature. For example, a function that converts Celsius to Fahrenheit should receive the temperature and return the converted value. Printing inside that conversion function couples a calculation to the terminal and makes it harder to reuse in a test, a graphical interface, or a file-processing program.

Ask what a caller needs to know: what information must it provide, what result will it receive, and which errors can occur? A function should ideally have one coherent responsibility. If its name contains “and” or it requires many unrelated parameters, it may be doing several jobs that deserve separate functions.

Choose parameter passing deliberately

Pass small values such as integers and Booleans by value. The function receives its own value, so changing the local parameter does not change the caller's variable. Pass a larger read-only object by const T& when you do not need a copy. Use T& only when the function is expected to modify the caller's object, and make that effect clear in the function's name or documentation.

Returning a result often communicates a calculation better than modifying an output parameter. For example, returning std::string lets the caller decide whether to print, store, or transform the result. Modern C++ can return standard containers and strings efficiently; do not introduce pointer-based output parameters just to avoid ordinary return values.

Declarations and order of use

A function needs to be declared before a call can be checked. The declaration, also called a prototype, contains the return type, name, and parameter types. A definition adds the body. Small programs often put the definition above main; larger programs put the declaration in a header and the definition in a source file.

int multiply(int left, int right); // declaration

int main()
{
    return multiply(6, 7) == 42 ? 0 : 1;
}

int multiply(int left, int right) // definition
{
    return left * right;
}

Parameter names may differ between a declaration and a definition, but their types and order must agree. If they do not, the call may resolve to a different overload or the linker may report a missing definition. Keep declarations and definitions synchronized in multi-file programs.

Overloading, defaults, and return types

Overloading lets related functions share a name when their parameter lists differ in a meaningful way. The compiler chooses an overload based on the argument types. Too many overloads can make a call ambiguous, especially when conversions are involved. Changing only a function's return type does not create an overload because a call's arguments do not select a function by the value the caller plans to receive.

A default argument supplies a value when the caller omits a trailing parameter. Defaults are convenient for truly optional settings, but they can hide important choices. Put a default in one visible declaration and avoid repeating it in a definition. If several arguments are optional or related, a named configuration object may make calls more understandable.

Recursion and termination

A recursive function calls itself. Every recursive design needs a base case that returns without another recursive call and a step that makes progress toward that base case. Without progress, the calls continue until resources are exhausted. Recursion can express tree traversal naturally, but a loop is often simpler for a straightforward count, and deep recursion may consume substantial stack space.

Practice

Key idea: good function interfaces make data flow visible and give each operation one clear purpose.

Contracts, preconditions, and postconditions

A function contract describes what must be true before a call, what result or state change the function guarantees, and which failures it may report. For a square-root-like operation, the precondition might constrain the input domain; the postcondition relates the returned result to that input within a stated tolerance. Document assumptions that the type system cannot express, but prefer a stronger type or validated constructor when it can.

Preconditions should not be vague. “Valid input” is less useful than “the span contains at least one element and all values are finite.” At a public boundary, validate assumptions that callers cannot be trusted to satisfy. Inside a private function, an assertion may be appropriate for an invariant already guaranteed by its caller. Choose an error mechanism that fits the program: an explicit result, an exception, or a documented non-returning failure.

Overload resolution and conversions

When a function call has several candidates, the compiler performs overload resolution. It considers viable parameter lists and ranks conversions, including exact matches, promotions, and standard conversions. User-defined conversions and templates add further candidate rules. A call that appears obvious to a human may be ambiguous or select an unexpected overload if implicit conversions are involved.

Use explicit constructors and conversion operators to prevent surprising implicit conversions. Prefer distinct names when operations have materially different behavior. When a call is confusing, inspect the declared types, make the conversion explicit, and reduce the overload set rather than inserting casts until the code compiles.

Evaluation, exceptions, and function boundaries

Argument expressions and function calls have language-defined sequencing rules; do not make a call's correctness depend on modifying the same object in multiple argument expressions. Compute important intermediate values in named statements before the call. This is clearer and safer when argument evaluation or overloaded conversions are involved.

Exception safety is part of a function's contract. A function may provide no-throw behavior, the basic guarantee that invariants hold after failure, or the strong guarantee that failure leaves observable state unchanged. These guarantees matter for operations that mutate objects or acquire resources. RAII helps preserve invariants during stack unwinding, but the function still needs to decide which failures it propagates and what state callers may observe.

Advanced exercise

  1. Write a contract for a function that searches a range and returns either a position or a “not found” result.
  2. Create two overloaded functions where implicit conversions cause ambiguity, then redesign the interface with explicit types or distinct names.
  3. For a mutating operation that can fail, determine whether it provides the basic or strong exception guarantee and test the state after failure.
0% read