Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Values and functions

A C++ program is a graph of functions that exchange values. This book treats the function as the atomic unit of design: a named operation on values, written once and called from anywhere, not a method bolted to a class. Classes appear later and stay rare. If you come from Rust, read “function” as fn. If you come from C, returning rich values (tuples, structs, containers) is cheap and normal, so C’s out‑parameters and pointer‑returning habits fall away.

A function is one logical operation

A declaration tells the compiler a name’s type, and a definition supplies the body and must appear exactly once in the whole program (CG F.2). In a single-file example you write both at once. In a multi-file program the declaration lives in a header and the definition in one translation unit. Chapter 23 covers that split, and the one-definition rule that enforces it.

The guideline is deliberately narrow: one function does one logical operation.

Design each function to perform a single logical operation. This makes the function easy to test in isolation and reduces hidden coupling. For example, draw_triangle is a function, while draw_triangle_and_clear_screen_and_log is not. The compiler cannot enforce this rule. It relies on the programmer. When a function grows, extract helper functions so each piece remains testable and readable. Small functions can be inlined, eliminating call overhead.

Return by value is the default

C++ moves values, so returning a std::vector or a struct is not a copy of megabytes. The function builds the result in its own frame and the caller’s variable takes ownership of it through move, which for a temporary is elided to no copy at all. Write auto for the return type and let the compiler deduce it from the return expression:

#include <print>
#include <tuple>
#include <cmath>

// Solve a*x^2 + b*x + c = 0 and return the two roots.
// For the equation x^2 - 3*x + 2 = 0 the roots are 2 and 1.
std::tuple<double, double> solve_quadratic(double a, double b, double c) {
    double discriminant = b * b - 4.0 * a * c;
    double sqrt_disc = std::sqrt(discriminant);
    double root1 = (-b + sqrt_disc) / (2.0 * a);
    double root2 = (-b - sqrt_disc) / (2.0 * a);
    return {root1, root2};
}

int main() {
    // Structured binding decomposes the tuple into two distinct names.
    auto [root1, root2] = solve_quadratic(1.0, -3.0, 2.0);
    // Print exactly "roots: 2, 1".
    std::println("roots: {}, {}", root1, root2);
    return 0;
}

Multiple results come back as a std::tuple or, better, as a small named struct (CG F.21). You read them with a structured binding, which decomposes any aggregate (a tuple, a std::pair, an array, or a user-defined struct) into named locals in one line. The binding auto [root1, root2] is not a destructuring of a special tuple type, it is generic aggregate decomposition and you will lean on it for your own types in chapter 3.

Out-parameters (void solve(double*, double*)) are forbidden by the book’s style. They obscure the result in the argument list and break move semantics (CG F.20). Return the value.

Trailing return types, and why the book avoids them

The book writes the return type first, before the parameter list:

std::vector<int> make_squares(int n);

C++ also offers a trailing return type, written after the parameter list and preceded by ->:

auto make_squares(int n) -> std::vector<int>;

The book uses the leading form as its default. The trailing form reads right-to-left and hides the result behind the parameters, so it is not the book’s style. Yet the trailing form is required, or genuinely helpful, in a few real cases.

A trailing return type can name a parameter. In a leading return type the parameter names are not yet in scope, so you cannot write their types with decltype. The trailing form places the parameters first, which puts them in scope for the return type. This is the classic case where trailing is required:

template <typename T>
auto describe(T const& t) -> decltype(t.size());

The same scoping helps member function definitions. A trailing return type appears after the class name, so names looked up inside the class body are in scope. A leading return type sits before the class name and cannot see them:

struct Counter {
    std::size_t count() const;
};
auto Counter::count() const -> std::size_t; // return type sees the class

Returning a function pointer or an array reads badly with a leading type. The trailing form keeps the pointer or array syntax attached to the function name:

auto choose(int) -> int(*)(int); // returns a pointer to a function

Finally, the trailing form orders information the way the reader meets it: parameters first, result last. That ordering is the source of the “East End Functions” style. The book still prefers the leading return type for everyday code, and reserves the trailing form for the cases above where it is required or clearer.

#include <print>
#include <string_view>

// The return type is a trailing decltype of the parameter. Only the
// parameter name is in scope at that point, so this form is required.
template <typename T>
auto describe(T const& t) -> decltype(t.size()) {
    return t.size();
}

int main() {
    std::string_view greeting = "hello";
    std::println("size: {}", describe(greeting));
    return 0;
}

Overloading: one name, many shapes

Overloading lets several functions share a name, distinguished by argument types. C has no overloading. This is the first construct where C++ reads as a different language. The compiler picks the overload at compile time by best match: an exact match beats a promotion, a promotion beats a standard conversion, and a user-defined conversion is the last resort. The “best match wins” rule is all you need for everyday code. The full precedence machinery, including how templates enter the contest, waits for chapters 16 and 20.

#include <print>
#include <string_view>

void describe(int) {
    std::println("int overload");
}

void describe(double) {
    std::println("double overload");
}

void describe(std::string_view) {
    std::println("string overload");
}

int main() {
    describe(42);
    describe(3.14);
    describe(std::string_view{"hello"});
    return 0;
}

When the call sites are describe(42), describe(3.14), and describe("hello"), the compiler resolves each to a distinct function, and the test confirms the string overload fired. Overload resolution is zero-cost: the choice is made entirely before the program runs.

Overloading is the first place C++ diverges from C. C solves the same problem with name mangling: describe_int and describe_string are different names. C++ lets you use one name and trusts the compiler to pick. This is the same rule that templates extend in chapter 16, where the “shapes” become patterns and the compiler writes a new function for each type.

Default arguments state the common case

A parameter can carry a default used when the caller omits it. Defaults sit on the declaration and must be trailing: once a parameter has a default, every parameter to its right must too. A default encodes the usual call. The unusual call supplies the argument. Keep them for genuine common cases, not to merge several unrelated functions into one.

void greet(std::string_view name = "world") {
    std::println("hello, {}", name);
}

greet() prints hello, world. greet("Alice") prints hello, Alice. The default is compiled into each call site, so the two forms cost the same. (The multi-declaration rules for defaults across files belong to chapter 23.)

const is the default

const is a promise the compiler enforces. A const name cannot be rebound to a different value after initialization. Declare a local const unless it must change (CG ES.25). This is the same split as Rust’s let versus let mut, and the same discipline: start immutable, relax only where the algorithm demands it.

A const local variable binds once and never changes:

const double pi = 3.14159;
const int max_attempts = 3;

A const reference reads an object without copying or mutating it. Functions that merely read a value take it by const reference so they neither copy nor mutate (CG Con.1):

void show(const std::string& name) {
    std::println("{}", name);
}

The placement of const on a pointer decides what is fixed. A pointer to const (const T*) lets the pointer move but not the pointee. A const pointer (T* const) cannot be reseated, but the pointee stays mutable. A const pointer to const (const T* const) fixes both:

const int* p1;                // pointer to const, pointee read‑only
int* const p2 = &value;       // const pointer, cannot be reseated
const int* const p3 = &value; // const pointer to const

A const member function promises not to modify the object it runs on. The compiler checks this, so a const object can call it:

struct Counter {
    int value() const { return count_; }
    int count_ = 0;
};

A const return value protects the result from mutation. Returning const T by value is rare, because it blocks move semantics. The book returns plain values and reserves const for references and pointers:

const std::string label() const; // const return value, const member

A const function parameter promises the callee will not modify the argument. This is the default for read‑only parameters (chapter 8):

void draw(const Shape& shape);

Immutability‑by‑default is the cheapest correctness tool the language offers, and the book applies it everywhere.

Function naming and error handling

Name functions with a lower‑case verb phrase that describes the action, e.g., read_file or calculate_checksum. Avoid generic names. For recoverable errors return std::expected<T, E>. For unrecoverable failures throw an exception that includes a message identifying the failed operation.

References are aliases, not pointers

int& r = x; gives r a second name for x. Reads and writes through r reach the same object. A reference is not a pointer you must dereference, and it cannot be reseated after initialization. In Rust terms it is a reborrow.

A reference parameter hands the caller’s object to the callee, which can read or modify it. The full decision of what to pass (value, const reference, or std::span) is chapter 8. For now, the rule is that a reference means “I am” using your object,“ never “I own a copy.

Practical tips for writing functions

When designing a function, choose a clear verb‑phrase name, keep parameters short, pass large objects by const reference and small trivially copyable types by value, and return a value when needed.

Use void for functions that produce no observable result.

Common pitfalls

  • Do not return a reference to a local variable. The reference dangles after the function returns (see the WARNING above).
  • Do not store a pointer to a parameter that the caller can destroy before the function returns.
  • Avoid default arguments that depend on mutable global state, as they create hidden dependencies.
  • Keep overload sets small and well‑documented. Overloads that differ only by const qualification can be confusing.

Performance considerations

When a function returns a large object by value the compiler can apply return value optimization. This eliminates the temporary copy by constructing the result directly in the caller’s storage. Modern compilers can also apply named return value optimization when the return variable is a named local. The generated code frequently reduces memory traffic and improves cache usage.

If a function takes a large argument by value the copy can be expensive. Prefer a const reference for read‑only parameters. For parameters that the function will modify and then return, take the argument by value and move it back. This pattern enables the caller to pass a temporary without an extra copy. The move operation transfers ownership of the internal resources with minimal overhead.

Inlining small functions removes the call overhead entirely. The compiler decides whether inlining is beneficial based on the function size and the context of the call site. Functions that consist of a single return statement or a few arithmetic operations are prime candidates for inlining. Developers can hint at inlining with the inline keyword, but the final decision rests with the optimizer.

Do not include unnecessary branches inside hot loops. Branch prediction failures can stall the pipeline. When possible restructure code to keep the hot path straight. Use constexpr when the result can be computed at compile time. This moves work from runtime to compile time and can produce faster executables.

Profile the code with a sampling profiler to locate bottlenecks. Measure the impact of changes rather than assuming improvement. The guidelines in this chapter aim to produce clear, maintainable code, and the performance impact of each choice must be evaluated in the context of the whole program.

WARNING Never return a reference to a local variable. The local is destroyed when the function returns, and the caller receives a dangling name. Chapter 1 showed the compiler catching exactly this. Return-by-reference is reserved for assignment operators, which return *this (CG F.47).

Try this

Rewrite solve_quadratic to return a small user-defined struct { double first; double second; } instead of a std::tuple, and keep the call site a structured binding. In one sentence, state what this proves about structured bindings.