Masters-level programming lessons for professional software judgement.

Masters Programmes · Masters Software Engineering · Lesson 6

Calling Functions and Parameter Lists

Call C++ functions by constructing parameter lists that match the required types, then reason about overloads, conversions and nested calls.

Lesson overview

Calling a function correctly means building an argument list that matches the function's required parameter types. In C++, a function's identity is not only its name: its parameter types are part of its signature. A mismatch may fail compilation, trigger conversion, or select a different overload.

LevelMasters
ModeSeminar, code reading and compile-time reasoning
EvidenceA call-site audit with corrected parameter lists, overload notes and nested-call traces
SourceConverted from supplied Canvas lesson HTML

Learning objectives

  • Explain how function name and parameter types form a C++ function signature.
  • Check whether a call site's argument list matches the declared parameter list.
  • Trace nested function calls by evaluating inner expressions before outer calls.
  • Identify when overloads or templates are clearer than unsafe conversions.
  • Use compiler diagnostics and call-site review to protect memory layout and program meaning.

Key vocabulary before you start

Function signature

The name and parameter types used for overload matching.

Argument

The expression supplied at a call site.

Parameter

The named variable in the function declaration or definition.

Overload resolution

The compiler process that selects the best matching function.

Implicit conversion

A compiler-permitted type change that may or may not preserve intent.

Call trace

The ordered evaluation of nested function calls and returned values.

Call discipline map

A function call is a contract at the boundary between caller and callee. The argument expressions must produce values that fit the parameter list the function promises to accept.

C1

Match the signature

Check the function name and parameter types, not only the name.

C2

Compute arguments first

Each argument expression must be evaluated before the called function can run.

C3

Beware conversions

A compiling call may still be misleading if an implicit conversion changes meaning.

C4

Use overloads deliberately

Multiple signatures can be useful, but every call site should make the intended overload obvious.

Example: declaration, call and exact matching

This minimal example mirrors the supplied lesson: the declaration expects an int, and the call supplies an int, so the call site matches the parameter list.

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
    return 0;
}

Trace the state

  1. f1 is identified by its name plus its parameter type: int.
  2. The argument expression 2 has type int.
  3. The compiler can reserve parameter storage for x safely because the argument fits the parameter type.
  4. The returned int is used to initialise z.
Line or operationVariableValueState roleInvariant or check
Read the example before running itKey names and callsKnown from sourceReasoning setupEach named element should support the lesson focus: Calling Functions and Parameter Lists.
Trace the first meaningful operationPrimary state or boundaryEstablished by initialisation, call or conditionValid-state checkpointLater reasoning must not use the value before this checkpoint.
Identify the first decision or dereferenceControl or access pointDepends on current stateFailure-mode checkpointThe condition, pointer, argument or dependency must be valid before use.
Record the observable resultReturn value, output or mutationProduced by the exampleEvidence checkpointThe result should match the contract explained in the lesson.

Visual model: call-site matching pipeline

Use the pipeline to explain why a call compiles, fails or silently weakens intent.

Read the call siteIdentify the function name and each argument expression.

Evaluate nested expressionsInner calls produce values before outer calls can bind them.

Match the signatureParameter types, overloads and conversions determine whether a candidate is viable.

Bind valuesArguments initialise parameters inside the callee.

Return or mutateThe call produces a value, mutation or side effect that the caller must handle.

Call audit task

Choose one nested call and write the pipeline from innermost expression to final returned value.

Function calls and exact type matching

The compiler enforces type and size compatibility so that the value passed by the caller fits the memory representation expected by the callee.

This protects memory layout and prevents corruption. Any logic safety gained from strong matching is valuable, but the first duty here is representation safety.

At Masters level, the question is not merely whether a call compiles. The stronger question is whether the call site communicates exactly which operation is intended.

Applied task

Given a function declaration, write three call sites: one exact match, one compile-time mismatch and one compiling but risky implicit conversion.

A function is not just its name

C++ allows overloaded functions: the same name may be used for different parameter lists. That means a function's identity includes its parameter types.

A call to process(2) and a call to process(2.0) may select different overloads. The call site's argument types therefore influence which code actually runs.

A professional code review should inspect call sites and declarations together. Reading the name alone is not enough.

Applied task

Write two overloads with the same name and explain which one is selected by three different call sites.

Substitution and nested calls

The value for each parameter must be computed before the call can proceed. Parenthesised nested calls are evaluated from the inside outward for each nested expression.

In f1(f1(2)), the inner f1(2) produces a value that becomes the argument for the outer call. The outer call cannot begin until the inner call has supplied a compatible value.

Nested calls can be concise, but overuse damages readability. If a call chain hides which values are being produced and passed, introduce named intermediate values so the substitution sequence is visible.

Example: de-nest for clarity

This pair of examples computes the same final value, but the second form makes each produced value inspectable in a debugger and easier to discuss in review.

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);

Applied task

Trace f1(f2(f1(2))) as a sequence of produced values and parameter bindings.

Order of evaluation pitfalls

When a function has multiple arguments, do not rely on the order in which those argument expressions are evaluated. The standard gives the compiler freedom here, so separate arguments with side effects can behave unexpectedly.

The practical rule is simple: if two argument expressions mutate shared state, split them into named statements before the call. That gives the order a visible source-code form and makes the debugger trace obvious.

At this level, the issue is not only correctness. It is also professional communicability: a maintainer should not have to know compiler-specific evaluation behaviour to understand your call site.

Example: split side effects before the call

g() and h() both mutate i. Both increments happen, but their relative order as arguments is not something to design around.

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);

Applied task

Find a call with two mutating argument expressions and rewrite it so the state transitions are explicit.

Overloads, conversions and ambiguity

C++ allows function overloading: the same function name can have multiple parameter lists. The compiler selects the best match using exact matches, promotions and conversions.

Ambiguity is a useful failure mode because it tells you the call site does not communicate a single clear target. More dangerous is the call that compiles by conversion while quietly changing the programmer's intent.

Prefer explicit overloads or templates over relying on surprising implicit conversions. If a string-like call is meaningful, provide a string-like overload; if it is not meaningful, let the compiler reject it.

Example: overload selection

The name print is not enough to identify the function. The argument type decides which parameter list is selected.

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");  // error unless a string-compatible overload exists

Applied task

Design a small overload set and write example calls that prove each overload is selected intentionally.

Forward declarations and circular calls

Mutually calling functions need at least one forward declaration so the compiler knows the second function's signature when compiling the first.

A forward declaration solves the compile-time knowledge problem; it does not solve the algorithmic problem. If two functions call one another without a bounded stopping condition, the program can recurse until it fails.

Read mutually calling functions as both an interface question and a termination question: does each call bind to a known signature, and is there a clear route back to a base case?

Example: declaration before use

f1 can call f2 because f2 has been declared before f1 is compiled.

int f2(int); // forward declaration

int f1(int x) { // calls f2
    int y = 5;
    return f2(y);
}

int f2(int value) {
    return value + 1;
}

Example: forward declarations do not prove termination

This compiles once the functions are known to each other, but the cycle still needs a base case or a redesign that breaks the loop.

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.

Applied task

Explain the difference between a missing forward declaration error and an unbounded recursion design bug.

Choosing parameter passing style

Pick parameter types that express intent and cost. The caller should be able to tell whether a function reads a value, modifies the caller's object, or consumes ownership.

Use by value for cheap scalars and for objects you deliberately want to copy or move into the function. Use const& for read-only access to larger objects. Use non-const & only when mutation of the caller's object is the point of the call.

Rvalue references communicate consumption or move-oriented design. They are powerful, but they should make ownership clearer rather than merely making a signature look advanced.

Example: signatures communicate intent

Each signature tells the caller something about cost, mutability and ownership.

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

Applied task

Take one function signature and rewrite it in three forms: read-only, mutating and consuming. Explain how the call-site meaning changes.

Worked example: fix the calls

A professional refactor often starts by separating concerns that have been compressed into one line: nested substitution, side-effect ordering and parameter intent.

The goal is not to make code longer for its own sake. The goal is to make each value transition inspectable, each call target obvious and each argument evaluation order deliberate.

Once the call sequence is named, you can decide whether it should remain as local statements, become a helper function, or be redesigned as a clearer data transformation.

Example: identify problems, then refactor

The original calls compile, but they hide evaluation steps and rely on side-effecting argument expressions.

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 left = g();
    int right = h();
    int r2 = std::max(left, right);
}

Applied task

Explain which refactor improves readability, which one removes an evaluation-order hazard and which variable names should be made domain-specific.

Compiler diagnostics are boundary evidence

A parameter-list mismatch is not a cosmetic syntax issue. It is evidence that the caller and callee disagree about representation, intent or both.

Good diagnostics should be read as design feedback: does the call need a corrected argument, a safer conversion, a different overload or a redesigned interface?

The worst response is to force a cast merely to silence the compiler. A cast should explain an intentional boundary crossing, not hide a misunderstanding.

Applied task

Take one compiler error about a function call and rewrite it as a design question about caller/callee agreement.

Applied case lab

Case 1: The accidental overload

A call to score(2.0) selects score(double) instead of score(int). Explain how you would prove which overload ran and whether the call site should change.

Case 2: The unsafe cast

A developer casts a double to int to satisfy a function call. Decide whether the cast represents a domain rule, a bug, or a missing overload.

Case 3: The unreadable nested call

A nested call chain fits on one line but hides three intermediate meanings. Decide where named intermediate values would improve traceability.

Applied task: audit function calls

Practise checking signatures, tracing nested substitution and deciding when overloads or named intermediates improve safety.

Stage 1: read and classify

A. Match the declared parameter list

Decide which calls compile cleanly, which require conversion and which should be rejected or redesigned.

int markBand(int mark) { return mark / 10; }

double average(double total, int count) { return total / count; }

int a = markBand(72);
int b = markBand(72.5);
double c = average(180, 3);
double d = average("180", 3);
  • Classify each call as exact match, conversion, mismatch or poor design.
  • Explain what the compiler can protect and what the programmer must still judge.
  • Rewrite the questionable calls with clearer intent.
Reveal one possible refactor
int a = markBand(72);
int roundedMark = 73;
int b = markBand(roundedMark);
double c = average(180.0, 3);
// double d = average("180", 3); // reject: string is not a numeric total
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Stage 2: trace or repair

B. Trace nested calls

Evaluate the inner call first, then bind the produced value to the next parameter.

int f1(int x) { return x + 3; }
int f2(int y) { return y * 2; }

int result = f1(f2(f1(2)));
  • Write the sequence of calls in evaluation order.
  • Record the value produced by each call.
  • Refactor with named intermediate values if that improves readability.
Reveal one possible refactor
const int first = f1(2);      // 5
const int second = f2(first); // 10
const int result = f1(second); // 13
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Stage 3: justify the design

C. Choose overloads deliberately

Predict which overload is selected and decide whether the call site communicates the intention.

void logValue(int value) {}
void logValue(double value) {}
void logValue(const char* value) {}

logValue(4);
logValue(4.0);
logValue("4");
  • Identify the selected overload for each call.
  • Explain why "4" is not the same call as 4.
  • Add usage examples beside overload declarations to prevent accidental conversions.
Reveal one possible refactor
void logCount(int value) {}
void logMeasurement(double value) {}
void logLabel(const char* value) {}

logCount(4);
logMeasurement(4.0);
logLabel("4");
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Stage 4: test the boundary

D. Split side-effecting arguments

The call compiles, but the argument expressions mutate shared state. Make the order explicit before calling the function.

int i = 0;
int next(){ return i++; }

int result = combine(next(), next());
  • Explain why the two calls to next() make the call-site order hard to reason about.
  • Rewrite the code with named intermediate values.
  • Record the expected state transitions for i before and after each statement.
Reveal one possible refactor
int i = 0;
int next(){ return i++; }

const int first = next();
const int second = next();
const int result = combine(first, second);
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Stage 5: evaluate alternatives

E. Add the declaration before use

Repair the compile-time relationship between mutually aware functions, then check that the runtime relationship terminates.

int f1(int x) {
    return f2(x - 1); // f2 is not declared yet
}

int f2(int x) {
    return x <= 0 ? 0 : f1(x);
}
  • Add the minimum forward declaration needed for compilation.
  • Identify the base case that prevents unbounded recursion.
  • Explain why forward declarations do not, by themselves, make recursive logic safe.
Reveal one possible refactor
int f2(int); // forward declaration

int f1(int x) {
    return x <= 0 ? 0 : f2(x - 1);
}

int f2(int x) {
    return x <= 0 ? 0 : f1(x);
}
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

F. Choose the passing style

Rewrite these signatures so each one communicates whether it reads, modifies or consumes its argument.

int total(std::vector<int> values);       // copies everything
void addItem(std::vector<int> values, int item); // caller never sees the change
void rename(std::string name);             // unclear whether name is copied or consumed
  • Choose by value, const&, non-const & or && for each signature.
  • Explain the cost and intent trade-off for each choice.
  • Add const where the function should not mutate caller-owned state.
Reveal one possible refactor
int total(const std::vector<int>& values);       // read-only, no copy
void addItem(std::vector<int>& values, int item); // mutates caller-owned vector
void rename(std::string&& name);                  // consumes a temporary/moved string
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

G. Prove the call cycle terminates

Forward declarations make the functions visible to each other. They do not prove the runtime cycle is safe.

int f2(int);

int f1(int x) {
    int y = 5;
    return f2(y);
}

int f2(int x) {
    int y = 7;
    return f1(y);
}
  • Identify why this can recurse forever.
  • Add a decreasing measure and base case, or redesign as an iterative loop.
  • State the evidence you would use to prove termination.
Reveal one possible refactor
int f2(int);

int f1(int x) {
    if (x <= 0) return 0;
    return f2(x - 1);
}

int f2(int x) {
    if (x <= 0) return 0;
    return f1(x - 1);
}
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Quick checks

1. In C++, what forms a function signature for overload selection?

2. What happens first in f1(f1(2))?

3. What is the safest response to a call-site type mismatch?

AI-augmented practice notes

Use these as prompts for disciplined support, not as permission to outsource judgement.

Check call signatures
  • Paste a function and its call sites; ask AI to flag mismatched types and suggest minimal, safe fixes.
  • Ask AI to propose overloads or templates when multiple type variants are genuinely needed.
  • Ask AI to generate usage examples for each overload to prevent accidental conversions.
Nested call tracing
  • Ask AI to expand a nested call into named intermediate values.
  • Ask for a table of argument expressions, produced values and parameter bindings.
  • Use the result as a hypothesis, then confirm by reading the declarations and compiler diagnostics.
De-nest for clarity
  • Ask AI to rewrite deeply nested calls into named intermediate steps.
  • Ask it to extract a pipeline function when the intermediate names reveal a reusable operation.
  • Keep the final version readable enough that a debugger watch list can follow each produced value.
Spot side-effect hazards
  • Ask AI to find calls where multiple arguments mutate the same state, then split them safely.
  • Ask for pure helper functions that separate computation from mutation.
  • Use compiler warnings, tests and debugger traces to confirm the order you made explicit.
Safer overload sets
  • Ask AI to propose an overload or template set and generate example calls that prove resolution is unambiguous.
  • Ask it where explicit constructors or named functions would prevent accidental conversions at call sites.
  • Review the result against the domain meaning, not only against whether it compiles.
Prove termination
  • Ask AI to add base cases and a measure that strictly decreases toward termination.
  • Ask it to convert mutually recursive code into an iterative loop or state machine if that makes the behaviour clearer.
  • Treat the AI output as a proof sketch, then check the actual state transition yourself.
Pick the right passing style
  • Ask AI to rewrite signatures for intent: read-only, mutate caller-owned state, or consume ownership.
  • Ask it to add const where safe and remove accidental copies from hot paths.
  • Review every suggested reference or move against object lifetime and caller expectations.

Assessment tasks

  1. Audit five function call sites from a C++ file and classify each as exact match, conversion, overload selection or mismatch.
  2. Rewrite one nested call chain using named intermediate variables and explain whether readability improved.
  3. Find one call where multiple arguments have side effects and rewrite it to make the state transition order explicit.
  4. Create one forward-declaration example and explain the difference between compile-time signature knowledge and runtime termination.
  5. Refactor three function signatures so their parameter passing style communicates cost, mutability and ownership.
  6. Write a short termination argument for a mutually recursive pair of functions.
  7. Write a short note explaining why a compiling implicit conversion can still be a design smell.
  8. Create one usage example for each overload in a small overloaded function set.

Judgement questions

When do overloads clarify an interface, and when do they hide danger?

Answer using a real or invented pair of call sites.

What compiler diagnostic would you now treat as design feedback?

Choose a function-call error and explain the caller/callee disagreement.

When would you introduce a named intermediate value?

Discuss readability, debugging, repeated values and state traceability.