Programming glossary 55 terms mentioned in this lesson
Select any term for a clear definition. The return button brings you back to the exact term link you used.
Lesson: Functions in C++
Functions are structured entities that allow programmers to logically organise their code. Instead of rewriting the same sequence many times, you can group it into a function and reuse it with different input values. This improves clarity, maintainability, and correctness.
1) Why Use Functions?
Imagine you need to sort numbers at multiple points in your program. Instead of copying the sorting code everywhere, you write it once in a function. Then, whenever you need sorting, you call that function. The function does not change; only the parameters (data you give it) change.
// Function to sort a vector of integers
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void sort_numbers(vector<int>& nums) {
sort(nums.begin(), nums.end());
}
int main() {
vector<int> a = {4, 1, 3};
vector<int> b = {10, -2, 7};
sort_numbers(a); // reuse the function
sort_numbers(b);
for (int n : b) cout << n << " ";
}
Programming Insight (AI) — Spot Repetition
- Paste repeated code into AI and ask: “Can this be turned into a function?”
- AI can extract reusable code into parameterised functions with minimal effort.
- It can also propose function signatures that best match your use cases.
2) Features of a Function
- An identifier (name) is required so you can call it.
- Parameters must have known memory size at compile time.
- The return type defines what value is given back.
- Curly brackets enclose the body of the function.
- Anything declared inside ceases to exist when the function returns.
- Functions may return nothing (
void).
// Example: Function with parameters and return value
int add(int x, int y) {
return x + y;
}
int main() {
int sum = add(3, 5);
}
Programming Insight (AI) — Naming & Contracts
- AI can propose clearer function names (
calculateAveragevsavg). - Ask it to draft docstrings or comments explaining parameters and return values.
- Generate function contracts (preconditions/postconditions) to reduce misuse.
3) Functions, Memory, and Performance
Some novices avoid functions, fearing “extra jumps” in memory will hurt performance. This is a mistake. Compilers optimise function calls aggressively (inlining, cache-aware placement). As a beginner, you should always use functions when they make sense. Organised, correct code matters more than micro-performance tweaks.
// Compiler often inlines short functions
inline int square(int x) {
return x * x;
}
Programming Insight (AI) — Function & Performance
- Ask AI: “Would this function likely be inlined by the compiler?”
- Have AI generate benchmarks comparing function call vs inline vs macro.
- AI can explain compiler optimisation reports in plain English.
4) Guidelines for Writing Functions
- Do one clearly defined task.
- Be no more than ~15 lines if possible (shorter is usually clearer).
- Use meaningful names; comments should explain parameters/returns.
// Clear, single-purpose function
double calculate_average(const vector<int>& scores) {
int sum = 0;
for (int s : scores) sum += s;
return static_cast<double>(sum) / scores.size();
}
Programming Insight (AI) — Refactor Into Functions
- Paste long functions into AI; ask it to split into smaller, single-purpose ones.
- AI can check if functions are too long, too nested, or doing more than one task.
- Ask AI for alternative function signatures (e.g., return
std::optional<T>when a failure is possible).
5) Functions That Return Nothing
void print_message(const string& msg) {
cout << msg << endl;
}
Sometimes you want an effect (printing, logging) rather than a returned value. Such functions use the void return type.
Programming Insight (AI) — Effects vs Values
- Ask AI if your function should return a value instead of being
void. - AI can suggest splitting effectful code (I/O, logging) from pure computations.
Summary
Functions are the building blocks of structured programming. They provide reuse, clarity, and correctness. A good function:
- Has a clear name.
- Does one task.
- Has well-defined parameters and return type.
- Does not exceed its scope.
Use functions freely; let the compiler optimise. Focus on correctness and clarity, not premature performance worries.
Mini Exercise — Design, Refactor, and Debug Functions
Goal: practise extracting reusable functions, choosing clear signatures (parameters/returns), avoiding lifetime bugs, separating effects (I/O) from pure computation, and adding simple contracts.
-
Extract & generalise
The code repeats the same idea twice. Refactor intocompute_averageandprint_reportwith clear names and types.#include <iostream> #include <vector> using namespace std; int main() { vector<int> maths = {70, 60, 80}; vector<int> physics = {10, -2, 7}; int sum1 = 0; for (int s : maths) sum1 += s; double avg1 = sum1 / 3.0; cout << "Average: " << avg1 << (avg1 >= 50 ? " PASS" : " FAIL") << "\n"; int sum2 = 0; for (int s : physics) sum2 += s; double avg2 = sum2 / 3.0; cout << "Average: " << avg2 << (avg2 >= 50 ? " PASS" : " FAIL") << "\n"; }- Propose function signatures (parameters as
const &where appropriate; return a value, not print). - Ensure functions do one task each (compute vs print).
- Propose function signatures (parameters as
-
Pick the right parameter passing
Choose an API for “top-k sorted” that is clear and efficient. Fix the version below.#include <algorithm> #include <vector> // ❌ Copies input twice and sorts whole vector unnecessarily std::vector<int> top_k_sorted(std::vector<int> data, std::size_t k) { std::sort(data.begin(), data.end()); // O(n log n) if (k > data.size()) k = data.size(); return std::vector<int>(data.end()-k, data.end()); }- Refactor to take input by
const std::vector<int>&and avoid full sort (hint:std::partial_sort_copyornth_element). - Explain your parameter/return choices in one sentence.
- Refactor to take input by
-
Fix lifetime & side-effects
Identify the bug and rewrite safely.const int& largest_ref(const std::vector<int>& v) { int max = v.empty() ? 0 : v.front(); for (int x : v) if (x > max) max = x; return max; // ❌ returns reference to a dead local }- Return by value (
int) or return an iterator (std::vector<int>::const_iterator) instead. - Briefly justify: when is returning by value preferable?
- Return by value (
-
Separate effects from values
Split this function into a pure calculator and a printer; then write a tiny test for the pure function.#include <iostream> double avg_and_print(const std::vector<int>& scores) { // ❌ mixes concerns long long sum = 0; for (int s : scores) sum += s; double avg = scores.empty() ? 0.0 : (double)sum / scores.size(); std::cout << "Avg: " << avg << "\n"; return avg; }- Create
double calculate_average(const std::vector<int>&)andvoid print_average(double). - Show a call site that uses the value without printing.
- Create
-
Add a simple contract (preconditions)
Guard against empty inputs without crashing or dividing by zero.#include <optional> // Write: std::optional<double> safe_average(const std::vector<int>& scores); // Return empty optional if scores.size() == 0; otherwise the average.- Show example usage (if/has_value) and a fallback message when there’s no data.
-
Function vs macro (correctness first)
Explain the difference wheni == 2. Which is safer and why?#define SQUARE(x) ((x) * (x)) inline int square(int x) { return x * x; } int i = 2; int a = SQUARE(i++); // ? int b = square(i++); // ?
Programming Insight (AI) — Function Coach
- Extract helper: “Find repeated code and propose function boundaries + signatures.”
- API reviewer: “Should params be by value,
const&, or non-const&? What should this return?” - Contract writer: “Draft pre/postconditions and edge-case tests for each function.”
- Perf explainer: “Would the compiler likely inline this? Any needless copies?”
Lesson 4 extension · judge the boundary, not the line count
What has the function promised, and to whom?
Repeated code is easy to see, so removing repetition is often the first reason given for creating a function. It is a useful reason, but it is not the deciding one. Six repeated lines may belong to one operation, two different operations or no useful operation at all. Extracting them changes the shape of the program; it does not prove that the new shape is better.
The stronger test is the boundary the function creates. Its name and declaration should tell the caller what operation is available, which values must cross into it and what comes back. Its body must then honour that account. If a reader has to inspect the body to discover a hidden input or an unexpected change elsewhere in the program, the boundary has concealed a responsibility rather than controlled it.
This is why a function is more than a convenient container for statements. It is the first practical unit in which we can separate a claim from the evidence for that claim. The declaration makes the claim. The body supplies the evidence. Tests can then ask whether the evidence holds for the cases the function promises to handle.
A call is a sequence, not a mysterious jump
#include <iostream>
int square(int value) {
return value * value;
}
int main() {
const int sideLength{6};
const int area{square(sideLength)};
std::cout << "Area: " << area << '\n';
}
Begin at the call site. In square(sideLength), sideLength is the argument expression and its value is 6. The call creates and initialises the parameter object value for this invocation. Because this parameter is passed by value, value is a separate object. Changing it inside square would not change sideLength.
Control enters the function body. The expression value * value produces 36, and the return statement supplies that value to the call expression. The expression square(sideLength) therefore has the value 36, which is used to initialise area. The parameter value reaches the end of its lifetime when this call finishes, and control continues in main.
| Stage | Control location | What can be established |
|---|---|---|
| Before the call | main | sideLength has the value 6 |
| Parameter initialisation | Entering square | A new value object is initialised with 6 |
| Return expression | square | value * value produces 36 |
| After the call | main | The returned 36 initialises area |
The distinction between an argument and a parameter is not decorative terminology. The argument belongs to the caller. The parameter belongs to this invocation of the function. That distinction becomes essential when the course reaches copying, references and ownership. If the two words are treated as synonyms now, later explanations will have no precise way to say which object changed.
A returned value is also not printed output. square returns an integer to its caller and performs no input/output. main chooses to print the result. Replace the console with a graphical interface or a test, and the calculation remains usable because it has not acquired a printing side effect. Seeing 36 on the screen proves that some output occurred; it does not, by itself, prove that square returned 36 to its caller.
A compiler may inline a small function, but that implementation decision does not alter this semantic account. We still reason about an argument, a parameter, a body and a returned value. Performance is measured against a real program and a real requirement. It is not guessed from the mere presence of a function call.
Read the declaration as a partial contract
A declaration gives important evidence, but not the whole contract. For a small function, force the missing parts into the open with three questions:
- What must be true about the arguments when the function is called?
- What value or observable effect does the function promise?
- Which state, if any, is the function permitted to change?
Now test square against the type it advertises. On an implementation where a 32-bit int has a maximum value of 2,147,483,647, the mathematical result of square(50'000) is 2,500,000,000. The call is well-formed, but the signed multiplication overflows and C++ does not define the resulting behaviour. Successful compilation has proved the syntax and type checks that the compiler performed. It has not proved that the operation is valid for every int value.
The real design must therefore decide what the accepted input range is and how failure is represented. The function could reject an unsuitable value, use a wider checked representation or return an explicit failure. Until that decision is made, int square(int value) describes the types crossing the boundary but overstates the safe mathematical operation.
Return types require the same care. Suppose save_game() returns void. That does not mean nothing happened. The function may open a file, write bytes and update error state before returning no value. Those observable effects belong in its contract. Conversely, a function named calculate_score() that also resets the current level carries out an action its name and return type fail to admit. The caller needs to know what changes, not merely what comes back.
Coherence matters more than a preferred size
Line count is a proxy. A six-line function can mix unrelated decisions, while a longer function can still express one coherent operation at one level of abstraction. I want to know whether the function has one responsibility that can be named, tested and understood without keeping the whole program in mind.
process() tells me almost nothing. load_level_description() makes a narrower claim, but a better name cannot rescue a confused body. If the only honest name contains "and", that is evidence that two responsibilities may have been forced together. It is evidence, not an automatic splitting rule: the body and its effects decide the matter.
Every split creates an interface. A parameter carries an admitted dependency into the function. A return value or side effect carries an outcome out. A read from a hidden global is still a dependency, only one that the declaration has failed to reveal. Creating the greatest possible number of tiny functions would merely replace one kind of confusion with another. The useful decomposition is the one that makes important decisions visible and keeps each boundary truthful.
Generated code should be judged in this order as well. Begin with the proposed declarations. Do the boundaries match the responsibilities in the problem? Can every required input and every permitted mutation be accounted for? Is the promised result testable? Only then inspect the bodies. Elegant statements behind the wrong interface still produce the wrong design.
Programming Insight (AI): make the proposed boundary account for every read and write
An AI tool notices that six lines appear twice and offers do_lines(a, b, c, d, e). Repetition explains why the tool looked there; it does not justify the extraction. Write down what each of the five values means, every global the body reads, every object it changes and every result a caller can observe. Then compare that account with both original locations. If the two copies serve different responsibilities, a shared helper would remove duplicated syntax while creating a dishonest interface. Rejecting that helper is not resistance to reuse. It is a decision that the proposed boundary has failed its evidence test.
Test the two sides of the boundary
Choose one function declaration from the lesson. Without opening its body, write four short statements: what the caller supplies, which parameter objects are initialised, what value or effect is promised, and what other state may change. Then inspect every read, write and return in the body. Mark each place where the implementation requires more than the declaration and surrounding explanation admitted.
Finish by tracing two calls to square, first with 6 and then with 7. Give each invocation its own parameter object and follow each returned value to its destination. If your trace cannot distinguish the caller's argument from the callee's parameter, repair the vocabulary before adding more code. The purpose of the exercise is not to recite four function parts. It is to show that the claim at the boundary and the work inside it agree.
Reveal answer
I shall use int square(int value). The declaration establishes that one value suitable for initialising an int parameter crosses into the call and that an int value comes back. The surrounding explanation gives that returned value its intended meaning: the mathematical square of the supplied value.
- The caller supplies one argument expression whose value can initialise an
int. - Each invocation creates and initialises its own by-value parameter object called
value. - The function promises to return the square when that result is representable by
int. - The declaration alone does not prove the absence of hidden state changes. Inspection of this body shows that it changes no external object and performs no input or output.
The body reads value twice in value * value, writes no object and returns the product. It also exposes a requirement that the short declaration does not express: the multiplication result must fit in int. On a common implementation with a 32-bit int, values from -46,340 to 46,340 have representable squares. Portable code must derive its limit from the implementation rather than assuming that width.
| Invocation | Argument | Parameter object | Returned value | Destination |
|---|---|---|---|---|
square(6) | The literal produces 6. | A new value is initialised with 6. | 6 * 6 produces 36. | For example, const int first{square(6)}; initialises first with 36. |
square(7) | The literal produces 7. | A different value is initialised with 7. | 7 * 7 produces 49. | For example, const int second{square(7)}; initialises second with 49. |
The first parameter object reaches the end of its lifetime when its call finishes. The second call creates another parameter object with the same name. Neither call changes an object belonging to the caller, because the parameter is passed by value.