C++ Module 4 – Functions, files and scope
C++ functions: declarations, definitions, returns
Declare and define C++ functions, return several results in a struct, flag ignored results with [[nodiscard]] and see what a missing return does.
What you will learn
- Trace a call from its argument to the parameter and back to the returned value
- Declare a function before the first call, and fix an undeclared-name error
- Write a function that returns several results in a struct instead of filling output parameters
- Use [[nodiscard]] so that an ignored result makes the compiler warn
- Prevent a missing return with -Wreturn-type and -Werror=return-type
Before you start
What you’ll learn · Before you start
What you will learn
- Trace a call from its argument to the parameter and back to the returned value
- Declare a function before the first call, and fix an undeclared-name error
- Write a function that returns several results in a struct instead of filling output parameters
- Use [[nodiscard]] so that an ignored result makes the compiler warn
- Prevent a missing return with -Wreturn-type and -Werror=return-type
Before you start
On this page
A rule written out in three places has to be fixed in three places: change how marks become grade points, and every
copy must change the same way. A function gives the rule one name and one home. It takes its inputs as parameters,
does one job and hands back a result, so every caller uses it with one call, grade_points(92), and a fix happens
once.
You will define and call functions, declare them before use, return several results in a struct and make the
compiler warn when a result is ignored. You will also see what happens when a function forgets its return: one file
printed 1, printed 4, trapped and crashed in four builds. Every program was built with Apple clang 21 and GCC 16.
A call copies the argument, runs the body and returns a value
Here is a complete function and a main that calls it three times. Its first line says everything a caller needs:
int is the type of the result, grade_points is the name, and (int marks) is the list of
parameter: A variable named in a function's parameter list, such as marks in int grade_points(int marks). Each call creates it anew and initialises it from the argument the call passes.. The body, between the braces, runs on each call:
// grade_points.cpp: one function, defined above main and called three times.
#include <print>
// Turns marks out of 100 into grade points on a 10-point scale.
int grade_points(int marks) {
if (marks >= 90) return 10;
if (marks >= 75) return 8;
if (marks >= 60) return 6;
if (marks >= 40) return 4;
return 0;
}
int main() {
int maths = 92;
std::println("maths: {} marks, {} points", maths, grade_points(maths));
std::println("physics: 67 marks, {} points", grade_points(67));
int total = grade_points(81) + grade_points(39);
std::println("two more subjects: {} points together", total);
} Output
maths: 92 marks, 10 points physics: 67 marks, 6 points two more subjects: 8 points together
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined grade_points.cpp -o grade_points && ./grade_points
The program printed 10, 6 and 8 points, and the GCC build printed the same three lines. A return statement ends
the function at once and hands its value back; the rest of the body is skipped, which is why the five return lines
can follow one another without else. Step through the first call and watch where 92 goes:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The argument first. main reaches grade_points(maths). Before the call starts, its argument is evaluated: maths holds 92.
- 92 starts the call. The call creates a new variable for itself, the parameter marks, and initialises it with 92. It is a copy, so nothing the function does to marks can change maths.
- The body runs. The first test, marks >= 90, is true, so return 10 runs. A return ends the function at once: the other four tests are never evaluated.
- 10 comes back. The function hands 10 back, and marks, which belonged to this call, is gone. In main the expression grade_points(maths) now has the value 10.
- println uses the value. std::println receives 92 and 10 as arguments and prints maths: 92 marks, 10 points, the first line of the recorded output.
- Two calls in one sum. grade_points(81) + grade_points(39) makes two separate calls, each with a marks of its own. The standard does not say which of the two runs first, and here it does not matter.
- 8 and 0 make 8. The calls return 8 and 0, so total is 8, the third line the program printed. Each call started from its own copy of its argument.
When main evaluates grade_points(maths), the argument: The value a call passes to a function, such as maths in grade_points(maths). It is evaluated before the call starts and initialises the matching parameter. maths is evaluated first, and
its value, 92, initialises marks, a new variable that belongs to this one call. The body runs until a return, and
the call expression takes the returned value, 10, as if main had written 10 there. A call is an expression like
any other, so grade_points(81) + grade_points(39) simply adds 8 and 0.
Because marks is a copy, the function cannot change maths in main. Copying is how C++ passes arguments unless
you ask for something else; the parameter-passing lesson shows references, which let a function work on the caller’s
variable instead. A function that only does something, such as printing, has the return type void: it may end with
a bare return; or simply reach its closing brace.
Checkpoint
Check: What a call does with its argument and its result
The compiler must see a declaration before the call
The compiler reads a file from the top down, and it accepts a call only when it already knows the function’s name,
parameter types and return type. Here main comes first and calls a function defined below it:
C++ (call_too_early.cpp) · call_too_early.cpp
// call_too_early.cpp: main calls grade_points before the compiler has seen it.
#include <print>
int main() {
std::println("{} points", grade_points(92));
}
int grade_points(int marks) {
return marks >= 40 ? marks / 10 : 0;
} C++ (declare_first.cpp) · declare_first.cpp
// declare_first.cpp: a declaration at the top lets main call a function defined below.
#include <print>
int grade_points(int marks); // declaration: name, parameter types, return type, then ;
int main() {
std::println("{} points", grade_points(92));
}
int grade_points(int marks) { // definition: the same first line, then the body
if (marks >= 90) return 10;
if (marks >= 75) return 8;
if (marks >= 60) return 6;
if (marks >= 40) return 4;
return 0;
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra call_too_early.cpp -o call_too_early
g++-16 -std=c++23 -Wall -Wextra call_too_early.cpp -o call_too_early
clang++ -std=c++23 -Wall -Wextra declare_first.cpp -o declare_first && ./declare_first
""") Output
$ clang++ -std=c++23 -Wall -Wextra call_too_early.cpp -o call_too_early
call_too_early.cpp:5:31: error: use of undeclared identifier 'grade_points'
5 | std::println("{} points", grade_points(92));
| ^~~~~~~~~~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra call_too_early.cpp -o call_too_early
call_too_early.cpp: In function 'int main()':
call_too_early.cpp:5:31: error: 'grade_points' was not declared in this scope
5 | std::println("{} points", grade_points(92));
| ^~~~~~~~~~~~
[exit status 1]
$ clang++ -std=c++23 -Wall -Wextra declare_first.cpp -o declare_first && ./declare_first
10 points
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 errors_session.py
Apple clang says use of undeclared identifier 'grade_points'; GCC says 'grade_points' was not declared in this scope. Both point at line 5, column 31, where the call is. There are two fixes: move the definition above main, or
keep the order and add a function declaration: A function's return type, name and parameter types ended by a semicolon, as in int grade_points(int marks);. It lets code call the function before, or without, seeing its body. at the top, as declare_first.cpp does:
the first line of the function followed by a semicolon instead of a body. Step through what happens to that file,
from its first line to the running program:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A promise at line 4. The compiler reads the file from the top. Line 4 ends in a semicolon instead of a body, so it is a declaration: grade_points takes one int and returns an int.
- The call at line 7. main calls grade_points(92). The compiler looks the name up among the declarations above the call, finds line 4, and checks that 92 can initialise an int parameter.
- Without line 4. In call_too_early.cpp nothing above the call declares the name, so the lookup finds nothing: both compilers stopped at line 5, column 31, where the call is.
- The body at line 10. With the declaration in place, line 10 is the definition: the same first line followed by the body. It keeps the promise of line 4.
- One definition. A program may declare a function as often as it likes but must define an ordinary function exactly once. With several files, the linker joins each call to that one definition, and stops with an undefined symbol when there is none.
- It runs. The program runs and prints 10 points, the last line of the session.
A declaration promises that the function exists; the definition keeps the promise. The parameter names are optional
in a declaration (int grade_points(int); declares the same function), but they tell the reader what each argument
means. Declarations may be repeated as often as you like, while a program holds exactly one definition of each
ordinary function: the one-definition rule: The rule that a program holds exactly one definition of each ordinary function it uses. An inline function may be defined in every translation unit that uses it, as long as all the definitions are identical. More in “How C++ compiles: preprocess, compile and link”.
That is how programs of several files work. A header holds the declarations, every .cpp file that calls the
functions includes it, and one .cpp file holds the definitions. When a declared function has no definition
anywhere, every file still compiles, and the linker: The last stage of a build: it joins the object files of a program and the libraries it uses into one executable, matching every symbol a file uses with exactly one definition. More in “How C++ compiles: preprocess, compile and link” then reports an undefined symbol. The
exercise at the end of this lesson is built from three files in exactly this way.
Checkpoint
Check: Declarations, definitions and the order of a file
Return a struct when a function has several results
A function returns one value. When a computation produces several values that belong together, return a small struct that holds them. Older code, and many C libraries, use output parameter: A reference or pointer parameter that a function writes a result into, so the caller must create the variable before the call. Returning a value, or a struct of several values, is usually clearer. instead: the caller passes variables, and the function writes into them. Both programs split 155 minutes into hours and minutes, one each way:
Return a struct · split_struct.cpp
// split_struct.cpp: a function with two results returns them together in a struct.
#include <print>
struct Duration {
int hours;
int minutes;
};
Duration split_minutes(int total_minutes) {
return {total_minutes / 60, total_minutes % 60};
}
int main() {
Duration film = split_minutes(155);
std::println("The film runs {} h {} min", film.hours, film.minutes);
// a structured binding names both parts
auto [hours, minutes] = split_minutes(1000);
std::println("1000 minutes is {} h {} min", hours, minutes);
} Output
The film runs 2 h 35 min 1000 minutes is 16 h 40 min
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined split_struct.cpp -o split_struct && ./split_struct
Output parameters · split_out_params.cpp
// split_out_params.cpp: the same job with output parameters, as older code does it.
#include <print>
// The & makes hours and minutes refer to the caller's variables (see the next lesson).
void split_minutes(int total_minutes, int& hours, int& minutes) {
hours = total_minutes / 60;
minutes = total_minutes % 60;
}
int main() {
int hours; // must exist before the call, with no sensible value yet
int minutes;
// which arguments are inputs? The call does not say.
split_minutes(155, hours, minutes);
std::println("The film runs {} h {} min", hours, minutes);
} Output
The film runs 2 h 35 min
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined split_out_params.cpp -o split_out_params && ./split_out_params
Both print The film runs 2 h 35 min, but their calls read differently:
| At the call | Return a struct | Output parameters |
|---|---|---|
| What is written | split_minutes(155) |
split_minutes(155, hours, minutes) |
| Which values go in | The one in the brackets | Not shown by the call |
| The results | Arrive with their names | Need variables declared first |
Duration film = split_minutes(155); says what goes in and what comes out. split_minutes(155, hours, minutes);
looks like a call with three inputs, and it forces main to create hours and minutes before the call with no
sensible value in them. With the struct, auto [hours, minutes] = split_minutes(1000); names both parts in one line,
a A declaration such as auto [hours, minutes] = f(); that gives a name to each member of the struct, pair or tuple a call returns, in one line.,
which a later module covers with pairs and tuples. Rules F.20 and F.21 of the C++ Core Guidelines say the same
thing: hand results back as return values, and group several of them in a struct. Returning a struct is cheap too,
because C++ constructs the returned object right where the caller’s variable lives, as the parameter-passing lesson
measures.
When an output parameter is still right
A function that must change a large object the caller already has, such as a list of a million marks to sort in place, takes it by reference and changes it there. That is an in-out parameter, not a way to return new results.
Checkpoint
Check: Several results in a struct
[[nodiscard]] turns an ignored result into a warning
Some functions exist only for their result. If a caller throws that result away, the call was pointless, or worse, a
bug. Put the attribute [[nodiscard]] before the return type, and the compiler warns whenever the result of a call
is ignored. Line 20 below is the classic mistake:
// nodiscard.cpp: two functions whose results must not be thrown away.
#include <cctype>
#include <print>
#include <string>
[[nodiscard]] bool is_leap_year(int year) {
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}
// The reason in the brackets becomes part of the warning (C++20).
[[nodiscard("it returns a new string and leaves its argument alone")]]
std::string upper_case(std::string text) {
for (char& c : text) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
return text;
}
int main() {
std::string city = "Pune";
is_leap_year(2028); // the answer is computed and lost
upper_case(city); // a real bug: city is still "Pune"
std::println("{}", city);
city = upper_case(city); // the fix: keep the result
(void)is_leap_year(2028); // a cast to void says "ignored on purpose"
std::println("{}", city);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra nodiscard.cpp -o nodiscard && ./nodiscard
g++-16 -std=c++23 -Wall -Wextra nodiscard.cpp -o nodiscard && ./nodiscard
""") Output
$ clang++ -std=c++23 -Wall -Wextra nodiscard.cpp -o nodiscard && ./nodiscard
nodiscard.cpp:19:5: warning: ignoring return value of function declared with 'nodiscard' attribute [-Wunused-result]
19 | is_leap_year(2028); // the answer is computed and lost
| ^~~~~~~~~~~~ ~~~~
nodiscard.cpp:20:5: warning: ignoring return value of function declared with 'nodiscard' attribute: it returns a new string and leaves its argument alone [-Wunused-result]
20 | upper_case(city); // a real bug: city is still "Pune"
| ^~~~~~~~~~ ~~~~
2 warnings generated.
Pune
PUNE
$ g++-16 -std=c++23 -Wall -Wextra nodiscard.cpp -o nodiscard && ./nodiscard
nodiscard.cpp: In function 'int main()':
nodiscard.cpp:19:17: warning: ignoring return value of 'bool is_leap_year(int)', declared with attribute 'nodiscard' [-Wunused-result]
19 | is_leap_year(2028); // the answer is computed and lost
| ~~~~~~~~~~~~^~~~~~
nodiscard.cpp:6:20: note: declared here
6 | [[nodiscard]] bool is_leap_year(int year) {
| ^~~~~~~~~~~~
nodiscard.cpp:20:15: warning: ignoring return value of 'std::string upper_case(std::string)', declared with attribute 'nodiscard': 'it returns a new string and leaves its argument alone' [-Wunused-result]
20 | upper_case(city); // a real bug: city is still "Pune"
| ~~~~~~~~~~^~~~~~
nodiscard.cpp:12:13: note: declared here
12 | std::string upper_case(std::string text) {
| ^~~~~~~~~~
Pune
PUNE
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 nodiscard_session.py
upper_case takes a copy of its argument, changes the copy and returns it, so upper_case(city); leaves city as it
was, and the first line the program prints is Pune. Both compilers flag lines 19 and 20 with -Wunused-result, and
both quote the reason written in the brackets, which C++20 allows. The fix is to use the result:
city = upper_case(city);. To ignore a result on purpose, cast the call to void, as line 24 does: the cast tells
the compiler and the reader that the result was thrown away deliberately.
The warning appears without -Wall, because the standard recommends that compilers warn here. The C++23 standard
library marks functions such as empty() of std::string this way: name.empty(); only tests whether the string is
empty, so a line that was meant to clear it gets a warning. Mark your own functions [[nodiscard]] when they return
an error code, a new value or anything else whose loss is a bug, and leave it off functions such as std::printf,
whose result, the number of characters printed, callers rarely need: the printing is the reason for the call.
Expecting a function to change its argument
upper_case(city); computes a new string from a copy and throws it away. Keep the result,
city = upper_case(city);, or give the function a reference parameter if changing the caller’s string is the job.
Checkpoint
Check: [[nodiscard]] and ignored results
Running off the end of a non-void function is undefined behaviour
The C++ standard says that reaching the closing brace of a function that returns a value is
undefined behaviour: What a program does when it breaks a rule for which the C++ standard sets no requirements, such as reading past the end of an array. The compiler may assume it never happens, so the result can change with the compiler and the optimisation level. More in “Object lifetime and undefined behaviour in C++”: the language makes no promise at all about what the
program does next. Only main is exempt, and reaching the end of main means return 0;. Here is grade_points
without its last line, so marks below 40 run off the end:
// missing_return.cpp: grade_points minus its last line; marks below 40 run off the end.
#include <print>
int grade_points(int marks) {
if (marks >= 90) return 10;
if (marks >= 75) return 8;
if (marks >= 60) return 6;
if (marks >= 40) return 4;
} // flowing off the end of a function that returns int: undefined behaviour
int main() {
std::println("grade_points(95) = {}", grade_points(95));
std::println("grade_points(30) = {}", grade_points(30));
} from session import run
# The same file, built four ways. Both compilers warn, and each build still produces a program.
run("""
clang++ -std=c++23 -O0 missing_return.cpp -o clang_O0
clang++ -std=c++23 -O2 missing_return.cpp -o clang_O2 2> /dev/null
g++-16 -std=c++23 -O0 missing_return.cpp -o gcc_O0
g++-16 -std=c++23 -O2 missing_return.cpp -o gcc_O2 2> /dev/null
./clang_O0
./clang_O2
./gcc_O0
./gcc_O2
g++-16 -std=c++23 -Werror=return-type missing_return.cpp -o checked
""") Output
$ clang++ -std=c++23 -O0 missing_return.cpp -o clang_O0
missing_return.cpp:9:1: warning: non-void function does not return a value in all control paths [-Wreturn-type]
9 | } // flowing off the end of a function that returns int: undefined behaviour
| ^
1 warning generated.
$ clang++ -std=c++23 -O2 missing_return.cpp -o clang_O2 2> /dev/null
$ g++-16 -std=c++23 -O0 missing_return.cpp -o gcc_O0
missing_return.cpp: In function 'int grade_points(int)':
missing_return.cpp:9:1: warning: control reaches end of non-void function [-Wreturn-type]
9 | } // flowing off the end of a function that returns int: undefined behaviour
| ^
$ g++-16 -std=c++23 -O2 missing_return.cpp -o gcc_O2 2> /dev/null
$ ./clang_O0
grade_points(95) = 10
grade_points(30) = 1
$ ./clang_O2
grade_points(95) = 10
grade_points(30) = 4
$ ./gcc_O0
[stopped by signal 5 (SIGTRAP): a shell reports exit status 133]
$ ./gcc_O2
[stopped by signal 10 (SIGBUS): a shell reports exit status 138]
$ g++-16 -std=c++23 -Werror=return-type missing_return.cpp -o checked
missing_return.cpp: In function 'int grade_points(int)':
missing_return.cpp:9:1: error: control reaches end of non-void function [-Werror=return-type]
9 | } // flowing off the end of a function that returns int: undefined behaviour
| ^
cc1plus: some warnings being treated as errors
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 missing_return_session.py
Both compilers warn even without -Wall: -Wreturn-type is on by default in C++ for Clang and GCC. The four programs
were built anyway, and each did something different with grade_points(30):
| Build | What grade_points(30) did |
|---|---|
Apple clang, -O0 |
Printed 1: whatever was left where the result goes |
Apple clang, -O2 |
Printed 4: the test marks >= 40 was dropped |
GCC, -O0 |
Stopped with SIGTRAP, a trap put there on purpose |
GCC, -O2 |
Stopped with SIGBUS, after running past main |
The optimisers may assume that undefined behaviour never happens. Apple clang at -O2 therefore treated the path
without a return as impossible, concluded that marks >= 40 cannot fail, and gave every mark below 60 four points.
GCC at -O0 and -Og turns code that must never run into a trap instruction (its -funreachable-traps option). GCC
at -O2 worked out that grade_points(30) always runs off the end, so it deleted everything in main after the first
std::println, the end of main included, and the program ran on into the bytes that followed.
The two GCC builds printed nothing, not even the line for grade_points(95): when output goes to a file or a pipe it
waits in a buffer, and a program stopped by a signal never writes that buffer out. Shells such as bash and zsh
report a program stopped by signal n as exit status: The number a program hands back when it ends, the value main returns. 0 means success; a shell keeps the last status in $?, and && runs the next command only after a status of 0. More in “How C++ compiles: preprocess, compile and link” 128 + n (POSIX promises only a
value above 128), so 133 and 138 here. The last command of the session shows the cure: with -Werror=return-type
the warning becomes an error, and no program is built.
Key idea
Every path through a function that returns a value must end in a return. Build with -Werror=return-type, so
that a missing one stops the build instead of the program.
Checkpoint
Check: A missing return
Small functions with one job are easier to change and test
Programs that do everything in main work until someone has to change them. This one prints an electricity bill for
every meter reading on standard input, with a slab tariff invented for the lesson: the rate per unit rises after 100
and 200 units, a fixed charge rises above 300 units, and a duty of 5 % applies to both. Both versions read the same
seven readings:
One long main · bill_long.cpp
// bill_long.cpp: an electricity bill for every meter reading on standard input, all
// inside main. The tariff is invented for this lesson: 3.00 a unit up to 100 units,
// 4.50 a unit from 101 to 200, 6.00 a unit above 200; a fixed charge of 40.00 (75.00
// above 300 units); a 5 % duty on both.
#include <iostream>
#include <print>
#include <string>
int main() {
std::println("{:<8}{:>7}{:>10}{:>8}{:>8}{:>10}", "Meter", "Units", "Energy", "Fixed", "Duty", "Total");
std::string meter;
int units = 0;
int bills = 0;
int rejected = 0;
double billed = 0.0;
double highest = 0.0;
std::string highest_meter;
while (std::cin >> meter >> units) {
if (units < 0) {
std::println("{:<8}{:>7} rejected: a reading cannot be negative", meter, units);
++rejected;
continue;
}
if (units > 100000) {
std::println("{:<8}{:>7} rejected: check the meter", meter, units);
++rejected;
continue;
}
double energy = 0.0;
int rest = units;
if (rest > 200) {
energy += (rest - 200) * 6.00;
rest = 200;
}
if (rest > 100) {
energy += (rest - 100) * 4.50;
rest = 100;
}
energy += rest * 3.00;
double fixed = 40.00;
if (units > 300) {
fixed = 75.00;
}
double duty = (energy + fixed) * 0.05;
double total = energy + fixed + duty;
std::println("{:<8}{:>7}{:>10.2f}{:>8.2f}{:>8.2f}{:>10.2f}", meter, units, energy, fixed, duty, total);
++bills;
billed += total;
if (total > highest) {
highest = total;
highest_meter = meter;
}
}
std::println("{} bills, {} rejected, {:.2f} billed in all", bills, rejected, billed);
if (bills > 0) {
std::println("highest bill: {} ({:.2f})", highest_meter, highest);
}
} Input (standard input)
A-101 87 A-102 100 B-201 101 B-202 245 C-301 -12 C-302 412 D-401 250000
Output
Meter Units Energy Fixed Duty Total A-101 87 261.00 40.00 15.05 316.05 A-102 100 300.00 40.00 17.00 357.00 B-201 101 304.50 40.00 17.23 361.73 B-202 245 1020.00 40.00 53.00 1113.00 C-301 -12 rejected: a reading cannot be negative C-302 412 2022.00 75.00 104.85 2201.85 D-401 250000 rejected: check the meter 5 bills, 2 rejected, 4349.62 billed in all highest bill: C-302 (2201.85)
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined bill_long.cpp -o bill_long && ./bill_long
Five functions · bill.cpp
// bill.cpp: the same program with five functions, each doing one job, and a short main.
#include <iostream>
#include <print>
#include <string>
#include <string_view>
struct Bill {
double energy;
double fixed;
double duty;
double total;
};
// The invented tariff of this lesson, one slab at a time.
double energy_charge(int units) {
double charge = 0.0;
if (units > 200) {
charge += (units - 200) * 6.00;
units = 200;
}
if (units > 100) {
charge += (units - 100) * 4.50;
units = 100;
}
return charge + units * 3.00;
}
double fixed_charge(int units) {
return units > 300 ? 75.00 : 40.00;
}
Bill make_bill(int units) {
double energy = energy_charge(units);
double fixed = fixed_charge(units);
double duty = (energy + fixed) * 0.05;
return {energy, fixed, duty, energy + fixed + duty};
}
// Why a reading cannot be billed, or an empty string when it can.
std::string rejection_reason(int units) {
if (units < 0) return "a reading cannot be negative";
if (units > 100000) return "check the meter";
return "";
}
void print_bill(std::string_view meter, int units, Bill bill) {
std::println("{:<8}{:>7}{:>10.2f}{:>8.2f}{:>8.2f}{:>10.2f}", meter, units, bill.energy, bill.fixed, bill.duty, bill.total);
}
int main() {
std::println("{:<8}{:>7}{:>10}{:>8}{:>8}{:>10}", "Meter", "Units", "Energy", "Fixed", "Duty", "Total");
std::string meter, highest_meter;
int units = 0, bills = 0, rejected = 0;
double billed = 0.0, highest = 0.0;
while (std::cin >> meter >> units) {
std::string why = rejection_reason(units);
if (!why.empty()) {
std::println("{:<8}{:>7} rejected: {}", meter, units, why);
++rejected;
continue;
}
Bill bill = make_bill(units);
print_bill(meter, units, bill);
++bills;
billed += bill.total;
if (bill.total > highest) {
highest = bill.total;
highest_meter = meter;
}
}
std::println("{} bills, {} rejected, {:.2f} billed in all", bills, rejected, billed);
if (bills > 0) std::println("highest bill: {} ({:.2f})", highest_meter, highest);
} Input (standard input)
A-101 87 A-102 100 B-201 101 B-202 245 C-301 -12 C-302 412 D-401 250000
Output
Meter Units Energy Fixed Duty Total A-101 87 261.00 40.00 15.05 316.05 A-102 100 300.00 40.00 17.00 357.00 B-201 101 304.50 40.00 17.23 361.73 B-202 245 1020.00 40.00 53.00 1113.00 C-301 -12 rejected: a reading cannot be negative C-302 412 2022.00 75.00 104.85 2201.85 D-401 250000 rejected: check the meter 5 bills, 2 rejected, 4349.62 billed in all highest bill: C-302 (2201.85)
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined bill.cpp -o bill && ./bill
readings.txt
A-101 87
A-102 100
B-201 101
B-202 245
C-301 -12
C-302 412
D-401 250000 The two versions printed the same ten lines, and so did their GCC builds. The split version is a few lines longer, and that is fine, because every piece now has a name. To find the pieces, look for the comments and blank lines in a long function: each commented block is a function waiting to be named after its comment. Step through one reading, B-201 with 101 units, on its way through the functions:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Read one line. The loop in main reads the meter B-201 and 101 units from standard input, then asks rejection_reason(101) whether the reading can be billed.
- Billable. 101 is neither negative nor above 100,000, so rejection_reason returns an empty string, and main goes on to make the bill.
- The energy charge. make_bill(101) calls energy_charge(101): the 1 unit above 100 costs 4.50 and the first 100 units 3.00 each, so it returns 304.50.
- The fixed charge. fixed_charge(101) returns 40.00, because 101 units is not above 300.
- Duty and total. make_bill adds 5 % duty on 344.50, which is 17.225, and returns a Bill of four amounts with the total 361.725. Nothing has been printed yet.
- One row printed. main passes the Bill to print_bill, which prints B-201, 101, 304.50, 40.00, 17.23 and 361.73: the amounts rounded to two decimals.
- Each part alone. Every box can now be called on its own: a test checks make_bill(101).total against 361.725 without typing any input, and a new tariff changes energy_charge only.
energy_charge holds the slab tariff and nothing else, so a change of rates touches one place. make_bill returns a
Bill struct with all four amounts, so it can be checked without typing any input: a test can call it with 0, 100,
101, 200, 201 and 301 units, the edges of every slab. And main now reads like a summary of the program.
A few habits make functions easy to read and test: one job per function, a name that says what it returns or does
(fixed_charge, print_bill), few parameters, and results returned rather than printed, so that the caller decides
what to do with them.
Checkpoint
Check: Splitting a program into small functions
Practise: three functions about primes
The exercise has three files: a header with the declarations, a source file where you write the definitions, and the public tests.
C header · exercise/primes.h
// primes.h: the three functions of the exercise. Keep these declarations as they are.
#pragma once
// True when n is prime: greater than 1, with no divisor other than 1 and itself.
[[nodiscard]] bool is_prime(int n);
// The smallest prime number greater than n (next_prime(13) is 17).
[[nodiscard]] int next_prime(int n);
// How many prime numbers are less than or equal to limit (count_primes_up_to(10) is 4).
[[nodiscard]] int count_primes_up_to(int limit); primes.cpp (starter) · exercise/primes.cpp
// primes.cpp: your definitions. Each compiles, and gives wrong answers until you write it.
#include "primes.h"
bool is_prime(int n) {
(void)n; // remove this line when you use n
return false;
}
int next_prime(int n) {
return n + 1;
}
int count_primes_up_to(int limit) {
(void)limit;
return 0;
} exercise/primes_test.cpp
// primes_test.cpp: the public tests. Build them together with your primes.cpp.
#include <cstdlib>
#include <print>
#include <string_view>
#include "primes.h"
int checks = 0;
int failures = 0;
void check(bool passed, std::string_view what) {
++checks;
if (!passed) {
++failures;
std::println("FAIL: {}", what);
}
}
int main() {
check(!is_prime(-7), "-7 is not prime");
check(!is_prime(0), "0 is not prime");
check(!is_prime(1), "1 is not prime");
check(is_prime(2), "2 is prime");
check(is_prime(3), "3 is prime");
check(!is_prime(91), "91 is not prime (7 x 13)");
check(is_prime(97), "97 is prime");
check(!is_prime(1'000'000), "1000000 is not prime");
check(is_prime(2'147'483'647), "2147483647 is prime");
check(next_prime(-5) == 2, "next_prime(-5) is 2");
check(next_prime(2) == 3, "next_prime(2) is 3");
check(next_prime(13) == 17, "next_prime(13) is 17");
check(next_prime(89) == 97, "next_prime(89) is 97");
check(count_primes_up_to(1) == 0, "no primes up to 1");
check(count_primes_up_to(10) == 4, "4 primes up to 10");
check(count_primes_up_to(100) == 25, "25 primes up to 100");
check(count_primes_up_to(100'000) == 9'592, "9592 primes up to 100000");
if (failures == 0) {
std::println("All {} checks passed", checks);
return EXIT_SUCCESS;
}
std::println("{} of {} checks failed", failures, checks);
return EXIT_FAILURE;
} - Save the three files in one folder and build the tests with the starter file. They fail, as the session below shows: 10 of the 17 checks fail before you have written anything.
- Write
is_prime, thennext_primeandcount_primes_up_to, both of which can callis_prime. Rebuild withclang++ -std=c++23 -Wall -Wextra primes.cpp primes_test.cpp -o primes_test(org++-16) until it printsAll 17 checks passed. - Build once more with
-fsanitize=undefinedadded and run the tests again: noruntime errorline may appear.
from session import run
# The tests built with the starter file: every function still gives a wrong answer.
run("""
cd exercise && clang++ -std=c++23 -Wall -Wextra primes.cpp primes_test.cpp -o primes_test && ./primes_test
""") Output
$ cd exercise && clang++ -std=c++23 -Wall -Wextra primes.cpp primes_test.cpp -o primes_test && ./primes_test FAIL: 2 is prime FAIL: 3 is prime FAIL: 97 is prime FAIL: 2147483647 is prime FAIL: next_prime(-5) is 2 FAIL: next_prime(13) is 17 FAIL: next_prime(89) is 97 FAIL: 4 primes up to 10 FAIL: 25 primes up to 100 FAIL: 9592 primes up to 100000 10 of 17 checks failed [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 primes_session.py
A first hint: use trial division: Testing whether a number n is prime by dividing it by 2, 3, 4 and so on. A divisor larger than the square root of n pairs with one below it, so the test can stop once d * d is greater than n. More in “C++ practice set: loops, conditions and numbers”, testing divisors d from 2 while d <= n / d.
The obvious condition d * d <= n overflows int on the check for 2,147,483,647, the largest int on these
compilers, because 46,341 × 46,341 is larger than that, and signed overflow is undefined behaviour too. The sanitizer
build of step 3 catches it.
Once is_prime and its two partners pass, try the questions below on calls, declarations, returned structs,
warnings and small functions. They adapt to your answers, opening with whichever of the six ideas has tripped you up
most, and a wrong choice names the slip behind it:
Practice that adapts to you
Practise this lesson
13 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What does [[nodiscard]] do, and how do you ignore such a result on purpose?
[[nodiscard]] on a function, or on a type, asks the compiler to warn whenever the result of a call is thrown away,
as in upper_case(city);, which computes a new string and loses it. Apple clang and GCC both warn with
-Wunused-result, even without -Wall, and since C++20 a reason in brackets, [[nodiscard("...")]], becomes part of
the warning. The attribute changes no generated code; it only adds the warning, which -Werror turns into an error.
Use it for functions whose result is the whole point: error codes, new values, tests such as empty(). To discard a
result deliberately, cast the call to void: (void)is_leap_year(2028);.
A good answer usually earns a follow-up about types and headers. Try these two before you look at the reasons behind each choice:
Checkpoint
Interview round: two follow-up questions
Key takeaways
- A function has a return type, a name, a parameter list and a body; a call copies each argument into its parameter,
runs the body until a
return, and takes the returned value. - The compiler must see a declaration before a call. Declare as often as you need, and define each ordinary function exactly once.
- Return results instead of writing into output parameters, and return a struct when there are several.
[[nodiscard]]makes both compilers warn when a result is ignored; cast the call tovoidto ignore it on purpose.- Reaching the end of a non-void function other than
mainis undefined behaviour: one file printed 1, printed 4, trapped and crashed, so build with-Werror=return-type. - Short functions with one job and a clear name make a program easier to change, and their results can be tested without running the whole program.
Terms in this lesson
- argument
- The value a call passes to a function, such as maths in grade_points(maths). It is evaluated before the call starts and initialises the matching parameter.
- function declaration
- A function's return type, name and parameter types ended by a semicolon, as in int grade_points(int marks);. It lets code call the function before, or without, seeing its body.
- output parameter
- A reference or pointer parameter that a function writes a result into, so the caller must create the variable before the call. Returning a value, or a struct of several values, is usually clearer.
- parameter
- A variable named in a function's parameter list, such as marks in int grade_points(int marks). Each call creates it anew and initialises it from the argument the call passes.
References
- Working Draft, Programming Languages: C++ (N4950): 9.5 Function definitions, 9.12.10 Nodiscard attribute, 8.7.4 The return statement (ISO/IEC JTC1/SC22/WG21)
- Options to Request or Suppress Warnings: -Wreturn-type (GNU Project)
- Options That Control Optimization: -funreachable-traps (GNU Project)
- Diagnostic flags in Clang: -Wreturn-type (LLVM Project)
- Attributes in Clang: nodiscard, warn_unused_result (LLVM Project)
- C++ Core Guidelines: F.20 and F.21, return values and structs (Standard C++ Foundation)
- Bash Reference Manual: Exit Status (GNU Project)
- POSIX.1-2024: Shell Command Language (2.8.2 Exit Status for Commands) (The Open Group and IEEE)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress