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.
#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;
}| Part | Example | Contract role |
|---|---|---|
| Return type | int | Type produced for the caller |
| Name | square | Behavior'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.
#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
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.
#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';
}| Parameter | Meaning | Typical use |
|---|---|---|
T value | Independent local value | Small input or owned working copy |
T& value | Mutable alias | Required caller-visible modification |
const T& value | Read-only alias | Read a potentially expensive object without copying |
Watch out
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.
#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
voidwhen 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.
#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
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.
#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
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.
#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.
}| Scope | Begins | Ends |
|---|---|---|
| Block | At a declaration inside braces | At the matching closing brace |
| Function parameter | In the parameter declaration | At the function body's end |
| Namespace | At a namespace declaration | Across matching namespace declarations |
| Unnamed namespace | At its declaration | Source-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.
#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
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.
#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';
}factorial(1)returns 1factorial(2)waits for 1factorial(3)waits for 2main()waits for 6Watch out
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.
// 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
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.