C++ Part I · Module 6 · Functions
Designing a Function Interface
Every other chapter in this module answered a question with a measurement. This one has no new measurement to offer, because everything mechanical about functions is now established — how to declare one, what a call does with the arguments, three ways to take a parameter and three ways to hand something back, how to let a caller omit an argument, how one name can serve several types, and where a definition is allowed to live. What is left is the part that decides whether code written with all that is any good, and it is not a separate skill so much as a change in what you are optimising for. A signature is not a formality on the way to a body. It is the only part of a function that most of its callers will ever read, and every choice this module covered shows up in it: whether an argument is copied, whether the caller's object can change, whether a result can be ignored, whether a value has a default. That makes a signature a set of promises, and the whole of this chapter is the claim that those promises are worth designing deliberately rather than accumulating. The last three sections take one small function that works and is hard to use, and make it easy to use without changing what it does.
Intermediate16 min readC++Part IFundamentalsFunctions
Part I · Module 6 · Chapter 6.9 · Designing a Function Interface
1. What This Chapter Is and Is Not
Everything below is design convention, not language behaviour. Where a claim rests on a measurement, the measurement was made earlier in this module and is cited by chapter and section; nothing new is asserted about what the compiler does.
That distinction matters because the two kinds of statement deserve different treatment. Chapter 6.2's address measurement is true whether or not you agree with it. The rules here are arguments, and you should be able to say what each one buys — which is why each comes with its reasoning rather than as a list.
The frozen purpose names four things, and they are this chapter's four sections: clear contracts, one job each, honest names, and parameter forms that state intent.
2. The Signature Is the Contract
Chapter 6.1 §1 introduced a declaration as a contract and listed what a caller learns from it. By the end of this module that list has grown considerably. Here is everything a signature now tells a reader, with nothing but the signature available:
[[nodiscard]] double mean(const SampleBlock& block, bool ignore_zeros = false);| From this signature a caller knows | Because of |
|---|---|
a double comes back | the return type — 6.3 |
| ignoring the result is a warning | [[nodiscard]] — 6.3 §5 |
block is not copied | the reference — 6.5 §3 |
block will not be modified | the const — 6.5 §4 |
block may be a literal or a temporary | also the const — 6.5 §5 |
ignore_zeros may be omitted | the default — 6.6 |
the usual answer is false | the default's value, visible — 6.6 §3 |
Seven facts, none of them in a comment, every one of them checked. That is the standard to aim at: a caller should be able to use the function correctly from the declaration alone.
The negative version is the useful test. For each thing a caller must know, ask whether the signature says it. If the answer is "it is in the documentation" or "they will read the body", the signature is incomplete — and the documentation can drift where const cannot.
3. One Job Each
A function should do one thing. The difficulty is that "one thing" is not well defined, so here are two tests that are.
The name test. If an honest name needs the word "and", there are two functions. read_and_validate, parse_and_store, compute_and_log — each is telling you where to cut. This is not a rule about names; it is a rule about jobs that happens to be detectable in names.
The return test. If a function returns one thing and also modifies two of its arguments, a caller has three results to keep track of and no help from the signature about how they relate. One of those is the answer; the others are usually a different job.
The reason to care is specific rather than aesthetic:
A one-job function can be read in isolation. Chapter 6.3 §9's callout made the point in terms of guarantees: with by-value parameters and a return value, a reader of a call site knows nothing they own changed. Each extra job spends some of that.
A one-job function can be tested in isolation. A function that computes a value from its inputs can be checked by calling it. One that also writes to a global, prints, or mutates its arguments requires a setup and a teardown to check anything.
A one-job function is reusable by accident. The function you extract because it was cluttering another one is frequently the one you need again three weeks later. Functions designed for reuse are often wrong about what will be reused; functions that do one clean thing are right by default.
4. Honest Names
Chapter 6.4 §4 identified a problem that names have to solve. A reference parameter is invisible at the call site:
configure(2, result); // does this change result? you cannot tellBoth arguments look the same. Whether result is modified lives in a declaration that may be in another file. So the name is the only thing a reader of this line has.
That gives a concrete standard: the name should let a reader predict the effect without the declaration.
| Name | What a reader expects |
|---|---|
mean(block) | reads block, returns a number |
zero(block) | changes block |
fill(block, value) | changes block |
data(block) | nothing at all — it is a noun |
check(block) | unclear: returns a verdict, or fixes something? |
process(block) | unclear, and will stay unclear |
Three habits follow.
A verb for an action, a noun for a value. compute_mean or mean for something that returns; zero, reset, fill, scale for something that changes.
A predicate should read as a question. is_valid, has_errors, can_retry. A caller writing if (is_valid(block)) has a sentence; one writing if (valid(block)) has to guess whether it validates or asks.
Say the units and the frame when they are not obvious. timeout_ms, width_bits, offset_bytes. Chapter 3.4's argument was that when a width is part of a specification you should say the width; the same applies to a parameter name. A function taking int timeout invites exactly one bug, and it is the one where somebody passes seconds.
5. Parameter Forms That State Intent
This is the part where the module's measurements become a design rule. Chapter 6.5 §6's callout gave the table; here is why each line is a statement about meaning rather than about speed.
| Form | What it promises the caller |
|---|---|
int n | I take a copy. Nothing of yours changes. |
const SampleBlock& b | I read this and leave it alone. I do not copy it. |
SampleBlock& b | I may change this. |
| return value | here is the answer; nothing of yours moved |
| out-parameter | the answer is in something you own now |
Read that as five promises, and the design rule falls out: choose the form that makes the true promise, and the performance follows.
Chapter 6.5 §6 is the clearest case. const& is not chosen to be fast; it is chosen because "I read this and do not change it" is the true statement, and not copying comes along with it. A SampleBlock& on a function that only reads is the same speed and a false promise — and Chapter 6.5 §4 showed what the false promise costs: the compiler stops checking, now and against every future edit.
Two more design consequences of the module's measurements:
Use the return value for the answer. Chapter 6.3 §8's table: it is visible in the signature, it composes into expressions, and it lets the caller write const auto m = mean(block);. An out-parameter needs two statements and an object that exists in an undefined state in between.
Make failure part of the signature. Chapter 6.3 §9 measured the discipline: the failing call left its out-parameter at −1. If a function can fail, a caller should not be able to reach the result without passing the status — and the signature is where that is arranged.
6. A Worked Refactor: Before
Here is a function that works. It compiles cleanly, it produces the right answer, and it is unpleasant to use:
#include <iostream>
struct SampleBlock { int sample[8]; }; // Chapter 9.7's subject, borrowed
static void process(SampleBlock& b, int target, int& steps, bool& ok, int tolerance)
{
steps = 0;
int value = 0;
for (int v : b.sample) value += v;
while (value < target && steps < 100) {
value += value / 4 + 1;
++steps;
}
ok = (value - target) <= tolerance && value >= target;
for (int& v : b.sample) v = value / 8;
std::cout << "settled after " << steps << " step(s), value " << value
<< (ok ? ", within tolerance" : ", OUT OF TOLERANCE") << '\n';
}
int main()
{
SampleBlock block{};
for (int i = 0; i < 8; ++i) block.sample[i] = i + 1;
int steps = 0;
bool ok = false;
process(block, 40, steps, ok, 10);
return 0;
}Measured output:
settled after 1 step(s), value 46, within toleranceRun the four rules over it.
The contract is not in the signature. void process(SampleBlock&, int, int&, bool&, int) tells a caller almost nothing. Which int is the target and which the tolerance? Both are int and either order compiles.
It has four jobs. It sums the block, iterates toward a target, judges the result against a tolerance, overwrites the block, and prints. An honest name would need three "and"s.
The names are not honest. process predicts nothing. ok does not say what is ok. b says nothing at all.
The parameter forms misstate intent. SampleBlock& b is correct only because of the overwrite at the end — a caller cannot tell that reading is most of what happens. steps and ok are out-parameters carrying results that should be returned, and the caller has to create and initialize both before the call just to receive them.
And the printing is the worst of it: a function that computes something and also writes to the console cannot be used by anything that does not want console output.
7. What Each Rule Changes
Taking the rules in order:
One job each splits it. Summing, iterating, judging, overwriting and reporting are five things; the first four are separable and the fifth belongs to the caller.
The contract means each piece's signature says what it needs. A function that iterates toward a target needs the starting value and the target, not the block.
Honest names replace process with verbs that predict: sum_of, settle_toward, is_within.
Parameter forms make the reading read-only. sum_of takes const SampleBlock& — Chapter 6.5's form, for Chapter 6.5's reason. The results come back as return values rather than through int& and bool&.
One structural note. settle_toward needs to report both the final value and how many steps it took. Two results and no type to group them is exactly Chapter 6.3 §8's legitimate case for an out-parameter, so one stays — and it is the only one. Chapter 9.7's structs and Chapter 21.4's std::pair both remove even that reason later; this is what the right answer looks like with the tools available now.
8. A Worked Refactor: After
#include <iostream>
struct SampleBlock { int sample[8]; }; // Chapter 9.7's subject, borrowed
[[nodiscard]] static int sum_of(const SampleBlock& block)
{
int total = 0;
for (int v : block.sample) total += v;
return total;
}
// Returns the settled value; reports the iteration count through steps_taken.
[[nodiscard]] static int settle_toward(int start, int target, int& steps_taken)
{
int value = start;
steps_taken = 0;
while (value < target && steps_taken < 100) {
value += value / 4 + 1;
++steps_taken;
}
return value;
}
[[nodiscard]] static bool is_within(int value, int target, int tolerance)
{
return value >= target && (value - target) <= tolerance;
}
static void fill_with_average(SampleBlock& block, int total)
{
for (int& v : block.sample) v = total / 8;
}
int main()
{
SampleBlock block{};
for (int i = 0; i < 8; ++i) block.sample[i] = i + 1;
const int target = 40;
const int tolerance = 10;
int steps = 0;
const int settled = settle_toward(sum_of(block), target, steps);
const bool ok = is_within(settled, target, tolerance);
fill_with_average(block, settled);
std::cout << "settled after " << steps << " step(s), value " << settled
<< (ok ? ", within tolerance" : ", OUT OF TOLERANCE") << '\n';
return 0;
}Measured output — identical to the original:
settled after 1 step(s), value 46, within toleranceThe numbers are small enough to check by hand, which is worth doing once: the block holds 1 through 8, so sum_of gives 36; 36 is below the target of 40, so one iteration adds 36 / 4 + 1 and gives 46; 46 is now at or above 40 so the loop stops after one step; and 46 − 40 is 6, within the tolerance of 10.
That identity is the point. A refactor that changes behaviour is not a refactor, and the only honest way to present one is with the same output from both versions.
What the caller gained:
The call site reads as the algorithm. settle_toward(sum_of(block), target, steps) says what happens. The original's process(block, 40, steps, ok, 10) required reading the body to learn that 40 was the target and 10 the tolerance — and the named const locals in the new main remove even that question.
Three of the four computations are now const-initialized. settled, ok and the two inputs are each initialized once and never reassigned, which Chapter 3.9 and Chapter 5.2 §3 both argued for.
sum_of and is_within cannot touch anything. sum_of takes const&; is_within takes three ints by value. Chapter 6.5 §4's compile-time check now applies to every future edit of both.
[[nodiscard]] protects the three functions whose result is the point. Chapter 6.3 §5 measured it: ignoring one of those is now a warning.
The printing is at the call site, so every function here can be used by something that does not want output.
fill_with_average is the only function that modifies anything, and its name says so — Chapter 6.4 §7's rule.
9. What to Actually Do
Write the signature first. If you cannot write one a caller could use without the body, the job is not decided yet.
Cut where an honest name needs "and".
Name actions with verbs and values with nouns, and predicates as questions.
Put units in parameter names when they are not obvious — timeout_ms, not timeout.
Choose the parameter form that makes the true promise. const& because you do not modify, not because it is fast.
Return the answer. Reach for an out-parameter only for several results or a large caller-owned object, and then only once.
Mark [[nodiscard]] when the result is the reason to call.
Keep output at the call site. A function that prints cannot be used by a caller who does not want printing.
Check a refactor by output. Same input, same result, or it is not a refactor.
10. Mistakes Worth Making Once
Designing the body first and the signature afterwards. The signature then records what happened rather than what was wanted.
A function named process, handle or do_work. Each will still be unclear a year later.
Two int parameters whose order only the body reveals. process(block, 40, steps, ok, 10).
Using & where const& was true. Same speed, false promise, and the compiler stops checking — Chapter 6.5 §4.
Returning one result and modifying two arguments. Three results, no help from the signature.
Printing from inside a computation. It makes the function unusable anywhere that does not want output.
A long parameter list of same-typed values. The answer is a type that names the fields — Chapter 9.7.
Calling a behaviour change a refactor. Chapter 6.9's own example is only credible because both versions print the same line.
11. Interview Relevance
Design questions are usually open-ended, and the ones about functions reward a candidate who argues from the interface.
"How would you break this function up?" Use §3's two tests out loud — where an honest name would need "and", and how many results the caller has to track. That is a method rather than an opinion, which is what the question is looking for.
"Why const& here?" The strong answer is Chapter 6.5 §6's: because it is the true statement about what the function does, and the copy avoidance comes with it. A candidate who answers only "for performance" has the right code for the wrong reason, and will choose & the first time performance is not the concern.
"What makes a good signature?" That a caller can use the function correctly without reading the body. Then enumerate what a signature can say — return type, [[nodiscard]], reference, const, default — and note that each is checked where a comment is not.
12. What You Should Be Able to Say Now
- This chapter is design convention, resting on measurements made earlier in the module; nothing new is claimed about the compiler.
- The signature is the contract, and by now it carries seven separate facts — return type,
[[nodiscard]], reference,const, binding, default, default value. - The standard to aim at: a caller should be able to use the function correctly from the declaration alone.
- One job each, with two tests: an honest name needing "and", and a caller having to track more than one result.
- One job makes a function readable, testable and reusable in isolation.
- Names must predict the effect, because Chapter 6.4 §4 showed the call site cannot.
- Verbs for actions, nouns for values, questions for predicates, and units in parameter names.
- Each parameter form is a promise: by value takes a copy;
const&reads without copying;&may change this. - Choose the form that makes the true promise — Chapter 6.5 §6 — and the performance follows.
- Return the answer; use an out-parameter only for several results or a large caller-owned object.
- Make failure part of the signature — Chapter 6.3 §9's measured discipline.
- All four rules answer one question: how much must a caller know about the inside?
- Functions help to the extent their interfaces let you stop reading. Twenty functions with unclear contracts are worse than one long
main. - A refactor must produce the same output — §6 and §8 print the same line, which is the only reason §8 is evidence of anything.
13. Exercises
Exercise 1 — read the signature. List everything a caller learns from [[nodiscard]] bool try_read(const Address& a, std::uint32_t& out, int timeout_ms = 500);, and name the one thing they still cannot tell.
Exercise 2 — find the cut. Give an honest name for a function that reads a block, computes its mean, and writes the mean back into every element. Then say how many functions it should be and name each.
Exercise 3 — fix the names. Rename each and say what your name predicts: (a) void data(SampleBlock&) (b) bool check(const SampleBlock&) (c) int process(int, int) (d) void handle(int timeout).
Exercise 4 — correct the forms. For each, give the right parameter form and the promise it makes: (a) a function that reads a 16 KB block (b) one that zeroes it (c) one that reads an int (d) one that needs its own modifiable copy of a block (e) one that reads a block and may be passed a temporary.
Exercise 5 — count the results. In §6's process, list everything the caller receives and say which single one should have been the return value.
Exercise 6 — justify each change. Pick three differences between §6 and §8 and, for each, name the rule from §2–§5 that motivated it and the chapter whose measurement supports it.
Exercise 7 — your own refactor. Take void run(SampleBlock& b, int a, int c, bool& failed, int& count), invent a plausible job for it, and write the signatures only — no bodies — for the functions it should be. Say what each returns and what each promises about b.
14. Summary
- This chapter is convention built on the module's measurements, each cited.
- The signature is the contract, and it now carries seven checked facts.
- Aim at: usable correctly from the declaration alone.
- One job each — cut where a name needs "and", or where a caller tracks several results.
- Names must predict the effect, because the call site does not show a reference parameter.
- Verbs, nouns, questions, units.
- Every parameter form is a promise; choose the true one and the performance follows.
- Return the answer; out-parameters for several results or a large caller-owned object only.
- Make failure part of the signature.
- All four rules answer how much a caller must know about the inside, and the measured refactor in §6–§8 prints the same line both ways.
Module 6 is finished. You can now decompose a program into functions, say exactly what each call does with what it is given, and write a signature that tells a caller what they need without telling them how it works.
Which brings a question the module has been quietly deferring. Every example in it used static on every function, and Chapter 6.1 §3 asked you to read it as "not visible outside this file" and move on. Chapter 6.8 then needed it again and said the same thing. Meanwhile Chapter 6.2 said a parameter is created when a call starts and destroyed when it returns, and Chapter 6.3 §6 showed a returned reference to a local that the compiler warned about because the object was about to cease existing — without ever saying what governs when an object ceases to exist.
Those are three different questions that beginners answer with the same word: where a name is visible, how long an object lives, and whether other translation units can see it. Module 7 separates them, and the separation is what makes static, the dangling references of Module 8, and the header collisions you have already met explicable rather than things to be memorised.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
