Programming glossary 50 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: Calling Functions & Parameter Lists (C++)
Calling a function correctly means constructing the parameter list so it exactly matches the function’s required types. In C++, a function’s identity is not just its name: the parameter types are part of its signature. If the call’s parameter list doesn’t match, compilation fails (or calls a different overload).
1) Function Calls & Exact Type Matching
Declaration, then call:
int f1(int x) {
int y = 5;
return y; // demo only: ignores x
}
int main() {
int z = f1(2); // OK: argument type int matches parameter type int
}
The compiler enforces type/size compatibility so that the value you pass fits in the memory reserved for the parameter. This protects memory layout and prevents corruption; any logic safety you gain is a happy by-product.
Programming Insight (AI) — Check Call Signatures
- Paste a function and its call sites; ask AI to flag mismatched types and suggest minimal, safe fixes.
- Have AI propose overloads/templates when multiple type variants are needed.
- Ask AI to generate usage examples for each overload to prevent accidental conversions.
2) Substitution & Nested Calls
The value for each parameter must be computed before the call can proceed. Parenthesised, nested calls are evaluated “inside-out” for each nested expression:
int f1(int x) { return 5; }
int main() {
int z = f1( f1(2) ); // inner f1(2) is evaluated to produce the argument of the outer call
}
This enables concise code, but readability suffers when nesting is overused:
int f1(int){ return 5; }
int f2(int){ return 7; }
int z = f1( f2( f1(1) ) ); // hard to read
// Clearer:
int a = f1(1);
int b = f2(a);
int z2 = f1(b);
Programming Insight (AI) — De-nest for Clarity
- Ask AI to rewrite deeply nested calls into named intermediate steps.
- Have AI extract a pipeline function that documents the order of operations.
3) Order of Evaluation Pitfalls (Multiple Arguments)
When a function has multiple arguments, do not rely on the order in which those arguments are evaluated — the standard allows the compiler to choose. Side effects in separate arguments can therefore behave unexpectedly.
int i = 0;
int g(){ return i++; }
int h(){ return i++; }
// Unspecified relative evaluation order of arguments:
// either g() or h() may run first; 'i' is incremented twice, order unknown.
int r = std::max(g(), h());
// Safer:
int a = g();
int b = h();
int r2 = std::max(a, b);
Programming Insight (AI) — Spot Side-Effect Hazards
- Ask AI to find calls where multiple arguments mutate the same state, and split them safely.
- Have AI suggest pure helpers to separate computation from mutation.
4) Overloads, Conversions & Ambiguity
C++ allows function overloading — same name, different parameter types. The compiler selects the best match by exact match, promotions, then conversions. Ambiguities cause compile errors.
void print(int);
void print(double);
// void print(std::string_view); // uncomment to support strings
print(42); // calls print(int)
print(3.14); // calls print(double)
print('A'); // character promotes; likely print(int)
print("hi"); // ❌ ambiguous/missing unless string overload exists
Prefer explicit overloads (or templates) over relying on implicit conversions that can surprise callers.
Programming Insight (AI) — Safer Overload Sets
- Have AI propose an overload or template set and generate example calls that prove resolution is unambiguous.
- Ask AI to add
explicitconstructors to prevent accidental conversions at call sites.
5) Forward Declarations & Circular Calls
Mutually calling functions need at least one forward declaration so the compiler knows the second function’s signature when compiling the first. Beware unbounded mutual recursion (infinite back-and-forth).
int f2(int); // forward declaration
int f1(int x) { // calls f2
int y = 5;
return f2(y);
}
int f2(int x) { // calls f1
int y = 7;
return f1(y); // ❌ without a base case, this can recurse forever
}
Add termination conditions (base cases) or redesign to break the cycle.
Programming Insight (AI) — Prove Termination
- Ask AI to add base cases/measures that strictly decrease to guarantee termination.
- Have AI convert mutually recursive code into an iterative loop or a state machine if clearer.
6) Choosing Parameter Passing Style
Pick parameter types that express intent and cost:
- By value:
int f(int x)— cheap scalars; copies are fine. - By const reference:
int f(const std::string& s)— avoid copying large objects; no modification. - By non-const reference:
void f(std::vector<int>& v)— call intends to modifyv. - By rvalue reference:
void f(std::string&& s)— take ownership / move.
int sum(const std::vector<int>& v); // read-only, no copy
void append(std::vector<int>& v, int x); // modifies caller's vector
void set_name(std::string&& s); // consumes a temporary by move
Programming Insight (AI) — Pick the Right Passing Style
- Ask AI to rewrite signatures for intent (read-only vs modify vs consume).
- Have AI add
constwhere safe and remove accidental copies on hot paths.
7) Worked Example — Fix the Calls
Identify problems and refactor for clarity and safety:
int f1(int){ return 5; }
int f2(int){ return 7; }
int i = 0;
int g(){ return i++; }
int h(){ return i++; }
int main() {
int z = f1( f2( f1(1) ) ); // hard to read
int r = std::max(g(), h()); // side-effect order risk
// Better:
int a = f1(1);
int b = f2(a);
int z2 = f1(b);
int ga = g();
int hb = h();
int r2 = std::max(ga, hb);
}
Programming Insight (AI) — Automated Refactor Patch
- Ask AI for a patch that introduces intermediates, removes ambiguous conversions, and documents intent.
- Have AI generate unit tests ensuring the refactor preserves behaviour.
8) Quick Checklist — Safe, Readable Calls
- Match parameter types exactly (or add explicit, safe overloads).
- Avoid deep nesting; introduce named intermediates.
- Do not rely on argument evaluation order; separate side effects.
- Use forward declarations for mutual calls, but ensure base cases if recursive.
- Choose parameter passing style to reflect intent and cost.
Mini Exercise — Call Sites, Signatures & Safe Parameter Lists
Goal: practise matching exact parameter types, de-nesting for clarity, avoiding argument order pitfalls, designing unambiguous overloads, breaking circular calls safely, and choosing the right passing style.
-
Match the signature (no accidental conversions)
Fix the call sites so they match the intended parameter types without surprising promotions/narrowing.int scale_int(int x, int factor); double scale_double(double x, double factor); int main() { long long big = 10000000000LL; double d = 2.5; auto a = scale_int(42, 2.0); // ❌ mixed types auto b = scale_double(3, d); // ❌ relies on implicit int→double auto c = scale_int(big, 3); // ❌ potential narrowing // TODO: Write correct calls OR adjust function signatures responsibly. }- Show both approaches: (1) fix the calls with explicit casts; (2) refactor signatures to accept the real use-cases (e.g., templates or wider types). State which you chose and why.
-
De-nest for clarity (substitution still preserved)
Rewrite the nested calls with named intermediates, then extract a tiny “pipeline” function that documents the order.int f1(int x){ return x + 1; } int f2(int x){ return x * 2; } int f3(int x){ return x - 3; } int main() { int z = f1( f2( f3( f1(5) ) ) ); // ❌ hard to read // TODO: z2 using intermediates; then a function int pipeline(int) { ... } } -
Argument evaluation order: separate side effects
Make the result deterministic by removing side effects from argument expressions.#include <algorithm> int i = 0; int g(){ return i++; } int h(){ return i += 10; } // different side effect int main() { int r = std::max(g(), h()); // ❌ relative evaluation order unspecified // TODO: refactor to two statements (no side effects in the call). }- Explain (one sentence) why the standard allows either argument to be evaluated first.
-
Overloads & ambiguity: make intent obvious
Remove ambiguity without “mystery” conversions; prefer explicit overloads.#include <string> #include <string_view> void print(int); void print(double); // void print(std::string_view); // maybe needed? int main() { print(42); // ok print(3.14); // ok print('A'); // likely calls print(int) via promotion print("hi"); // ❌ ambiguous/missing // TODO: Add the right overload(s) and show unambiguous calls, including u8/u16 string literals if you wish. } -
Forward declarations & termination
Provide the minimum forward declaration and add a base case to ensure the mutual recursion stops.int f2(int); // forward declaration int f1(int x) { // calls f2 if (x <= 0) return 0; // TODO: sensible base case return f2(x - 1); } int f2(int x) { // calls f1 // TODO: matching base case / measure that decreases return f1(x - 1); }- Explain what “measure strictly decreases” means for proving termination here.
-
Choose parameter passing style to express intent
Refactor signatures to avoid copies when reading, and to make mutation/consumption explicit.#include <vector> #include <string> // ❌ unclear intent / unnecessary copies int sum(std::vector<int> v); void append(std::vector<int> v, int x); void set_name(std::string s); // TODO: Rewrite as: // int sum(const std::vector<int>& v); // read-only // void append(std::vector<int>& v, int x); // modifies caller // void set_name(std::string&& s); // consumes temporary- Give a one-line rationale for each choice (value / const& / & / &&).
-
Worked clean-up: fix the calls
Apply all the above: remove nesting, separate side effects, and make passing style explicit.#include <algorithm> int f1(int){ return 5; } int f2(int){ return 7; } int i = 0; int g(){ return i++; } int h(){ return i++; } int main() { int z = f1( f2( f1(1) ) ); // ❌ int r = std::max(g(), h()); // ❌ // TODO: // int a = f1(1); // int b = f2(a); // int z2 = f1(b); // int ga = g(); // int hb = h(); // int r2 = std::max(ga, hb); }
Programming Insight (AI) — Call-Site Coach
- Signature audit: “List each function’s overload set and show which call resolves where (or fails).”
- De-nesting pass: “Rewrite nested calls into intermediates; generate a
pipeline()function.” - Side-effect finder: “Flag multiple-argument calls where expressions mutate the same state.”
- Passing style fixer: “Suggest
const&/&/&&based on size, mutability, and ownership intent.”
Lesson 6 extension · select, transfer, return and verify
Which operation receives the call, and what crosses the boundary?
Parentheses make a function call easy to recognise. Recognition is not yet an explanation. At a call site, the compiler must find a callable declaration, decide which operation the arguments can use, initialise that operation's parameters and give the returned value a destination. Each step can be correct in isolation while the programmer has still called the wrong operation.
This is the distinction worth keeping: successful compilation proves that C++ found a permitted interpretation of the call. It does not prove that the interpretation matches the requirement. We need both accounts, the language account and the program account, and they must agree.
Follow one call all the way back to its receiver
#include <iostream>
int add(int left, int right) {
return left + right;
}
int twice(int value) {
return value * 2;
}
int main() {
const int subtotal{add(12, 8)};
const int total{twice(subtotal)};
std::cout << total << '\n';
}
Start with add(12, 8). The argument expressions produce the values 12 and 8. Those values initialise the separate parameter objects left and right for this invocation. The function body adds the parameter values, and return supplies 20 as the result of the call expression. That result then initialises subtotal.
The second call repeats the mechanism rather than sharing the first call's parameters. The expression subtotal produces 20, a new parameter object called value is initialised for twice, and the returned 40 initialises total. The parameter objects belong to their individual invocations and reach the end of their lifetimes when those calls finish.
| Call event | Values crossing in | Value crossing out | Receiver |
|---|---|---|---|
add(12, 8) | left becomes 12; right becomes 8 | 20 | subtotal |
twice(subtotal) | value becomes 20 | 40 | total |
We could write twice(add(12, 8)). Here the inner call must finish because its returned value is the argument required by the outer call. Read the data dependency from the inside out: 12 and 8 enter add, 20 leaves it and enters twice, then 40 leaves twice. This necessary nesting is different from assuming that separate arguments to one call are evaluated from left to right.
The named variable subtotal is not automatically superior, but it gives the intermediate value a meaning and a place to inspect it. Compression is useful only while the transfer of values remains obvious. If a debugger, diagnostic or human explanation needs the intermediate state, the extra name has earned its place.
Exact type matching is too simple to be the rule
Suppose the visible declarations include draw(int) and draw(double). For draw(2), both declarations can be considered, but the integer overload can accept the integer argument without conversion and is the better match. For draw(2.0), the double overload has the corresponding advantage. C++ has selected between viable candidates; it has not required every possible call to begin with identical argument and parameter types.
For these introductory examples, use three questions:
- Which declarations with this name are visible at the call site?
- Can the supplied arguments initialise their parameters?
- If more than one candidate is viable, which has the better conversion sequence?
This is deliberately a beginner's model rather than the full overload-resolution specification. It is still strong enough to replace the false instruction to match every type exactly. A permitted conversion may make a call viable, several viable candidates may leave an ambiguous choice, and no viable candidate leaves the call ill-formed. The return type alone does not choose between ordinary overloaded functions.
Now imagine an ambiguous call repaired by inserting static_cast<int>. The compiler becomes quiet. What has that proved? It proves that the cast steered selection towards an integer operation. It does not prove that discarding a fractional part or choosing that operation matches the problem. Decide the meaning first; then make the argument type or the interface state it. Silence from the diagnostic is necessary evidence, not sufficient evidence.
A forward declaration has a narrower purpose. It makes a name and type available before the definition appears. It does not run the function, reserve a permanent parameter object or solve a confused dependency. If two components require declarations from each other, the program may compile while the design still deserves inspection. Draw the dependency arrows and ask why both directions are needed.
Do not hide required order inside sibling arguments
Consider consume(read_next(), read_next()). One call to read_next will complete before the other, so their executions do not interleave. C++20 does not, however, require the left argument to be evaluated before the right argument. The body of consume begins only after both argument evaluations and parameter initialisations are complete.
// Avoid making the result depend on which argument is evaluated first.
consume(read_next(), read_next());
// Make the required order explicit.
const auto first = read_next();
const auto second = read_next();
consume(first, second);
If the program requires the first read to occur before the second, the original call has hidden a requirement that the language does not promise to honour. Separate statements make the sequence part of the source-level account. The problem was not nesting or multiple arguments by themselves. It was coordinating order-sensitive side effects through an unspecified relative order.
Notice the contrast with twice(add(12, 8)). The outer call cannot proceed until the inner call produces its argument, so the value dependency establishes the required order. In consume(read_next(), read_next()), neither sibling argument depends on the other's result. Their written position is not a portable sequencing instruction.
Parameter form should admit the responsibility
A by-value parameter creates a separate parameter object, as add and twice demonstrate. Changing that parameter does not change the caller's object. A reference to const refers to an existing object and prevents modification through that reference. A non-const reference permits the function to modify the caller's object, so that effect should be visible in the function's purpose rather than discovered by accident.
Cost matters, but intent comes first at this stage. Passing a small integer by value states the required relationship plainly. Passing a larger object by reference to const can avoid a copy while admitting observation rather than mutation. Pointer ownership and rvalue-reference policies belong with the later memory and object-lifetime material; adding advanced punctuation to a signature is not evidence of better design.
Programming Insight (AI): require a call account, not a compiling cast
When an AI tool proposes or repairs a call, require four pieces of evidence: the declarations visible at that point, the viable candidates, the conversion required for each argument and the selected overload. Compare its account with the compiler diagnostic and reduce the case when they disagree. If the proposed fix is a cast, ask which program requirement authorises the conversion and what information it may lose. "It compiles" describes one observation. It does not finish the review.
Build the call ledger
Trace twice(add(12, 8)) on paper. Give every invocation its own parameters, write the value returned at each boundary and name the expression or object that receives it. Then place draw(int) and draw(double) beside the calls draw(2) and draw(2.0). For each call, cross out the candidates that are not viable and explain why the selected conversion sequence is better.
Finish with consume(read_next(), read_next()). Write two permitted traces, one for each relative argument order, and decide whether the observable program result can differ. If it can, replace the call with named intermediate statements and state the order now guaranteed by the source. A useful call explanation accounts for selection, values, effects and return. Familiar parentheses account for none of them.
Reveal answer
The nested call
| Stage | Account |
|---|---|
add(12, 8) | The argument values 12 and 8 initialise separate parameters left and right. |
Inside add | left + right produces 20, and return makes 20 the value of the inner call expression. |
twice(...) | The returned 20 supplies the argument value that initialises a new parameter object called value. |
Inside twice | value * 2 produces 40, and the outer call returns 40. |
| Final receiver | In const int result{twice(add(12, 8))};, the returned 40 initialises result. |
The inner call must complete before its value can initialise the outer parameter. That dependency establishes the order; the nesting is not merely visual.
The overloads
| Call | draw(int) | draw(double) | Selection |
|---|---|---|---|
draw(2) | Viable with an exact match | Viable after converting int to double | draw(int), because the exact match has the better conversion sequence |
draw(2.0) | Viable after converting double to int | Viable with an exact match | draw(double), because the exact match has the better conversion sequence |
No candidate should be crossed out as non-viable in either of these calls. One candidate is less preferred. That is different from being impossible.
The sibling arguments
Suppose successive calls to read_next() produce 10 and then 20.
- If the left argument is evaluated first, the left expression receives 10 and the right expression receives 20, producing
consume(10, 20). - If the right argument is evaluated first, the right expression receives 10 and the left expression receives 20, producing
consume(20, 10).
C++20 requires one argument evaluation to complete before the other, but it does not specify which sibling argument is evaluated first. The observable result can therefore differ when consume distinguishes its first and second parameters, or when the reads themselves have observable effects.
const auto first = read_next();
const auto second = read_next();
consume(first, second);
The first declaration completes its read before the second declaration begins. The second read then completes before the call. The source now guarantees that the first value read becomes the first argument and the second value read becomes the second argument.