Lesson 5.4 — When a name denotes several things: overload resolution, argument-dependent lookup and macro hygiene¶
Techniques: overload resolution — candidate sets, viable functions, ranking of implicit conversion sequences and the best viable function (C++, ARM 1990 and ISO C++ [over.match]; Swift's solution ranking) — argument-dependent lookup (Koenig lookup; ISO C++ [basic.lookup.argdep]), and macro hygiene as name resolution by name and syntax context (Kohlbecker et al. 1986; recap of Lesson 4.8) · Pebble implements: none of the three (Pebble has no overloading, no namespaces and no macros;
writeare its only name with special resolution, pebble-spec §6.1, §10); the lesson runs on Clang 23.1.2 and rustc 1.94.1 · Prerequisites: Lessons 5.1–5.3 · Time: 3 hours
Lessons 5.1–5.3 assumed that a use of a name denotes exactly one declaration, chosen by where the use is. Three mechanisms break that assumption on purpose. Overloading lets one name denote a set of functions and picks one from the types of the arguments. Argument-dependent lookup makes that set depend on the arguments too: the namespaces of the argument types contribute candidates. And macros paste code written in one place into another, so a name's resolution must also depend on which expansion introduced it. All three turn name resolution from a function of the text position into a function of more information — types or syntax contexts — and all three are sources of famously confusing errors.
1. Problem and motivation¶
The problem. Given a call \(f(a_1, \dots, a_k)\) where lookup of \(f\) yields a set of declarations, choose one using the static types of the arguments — or report that none fits or that several fit equally well. For ADL, first decide which declarations are in the set. For hygiene, given an identifier that a macro expansion created, decide which binder it refers to so that macro-introduced and user-written names never capture each other by accident.
Overload resolution¶
Ada (1983) and C++ (1985) allowed several functions with the same name and different parameter types; the call picks one by comparing the implicit conversions each would need. The rules were formalized in the Annotated C++ Reference Manual [ES90, §13.2] and are now [over.match] in the ISO standard [CPP-Draft]: collect the candidate functions, keep the viable ones (right number of parameters, and a conversion exists for every argument), and choose the unique viable function that is better than all others argument by argument. Swift overloads functions and operators and resolves them inside its constraint solver, then ranks complete solutions by a score [SWIFT-TypeChecker]. Pebble does not overload: a name in the function namespace denotes one function (E0302 on a redefinition).
Argument-dependent lookup¶
In std::cout << "hi", the operator<< that prints a string is declared in namespace std, but the call site is outside it. C++ therefore extends unqualified lookup of a function name in a call with the namespaces associated with the argument types: argument-dependent lookup (ADL), also called Koenig lookup after Andrew Koenig, who proposed it to the C++ committee [CPP-Draft, basic.lookup.argdep]. It makes operators and customization points (swap, begin) work across namespaces, at the cost that adding an argument of a new type can silently change which function a call means.
Macro hygiene¶
A macro that expands to let tmp = e; tmp + tmp must not capture a tmp the user wrote inside e, and must not be captured by a tmp at the use site. Kohlbecker, Friedman, Felleisen and Duba [KFFD86] defined hygienic expansion: identifiers carry the expansion step that introduced them, and binding respects it. Lesson 4.8 gave the expansion algorithms (marks, sets of scopes); this lesson looks at the resolution side: a binding is now keyed by the pair (name, syntax context), which is how rustc resolves local variables (SyntaxContext in compiler/rustc_span/src/hygiene.rs).
2. Definitions and algorithms¶
Overload resolution¶
Definition 5.4.1 (Candidate, viable function, conversion rank)
For a call \(f(a_1, \dots, a_k)\), the candidate functions are the declarations that name lookup (plus ADL, below) finds for \(f\). A candidate with parameters \(p_1, \dots, p_m\) is viable if \(m = k\) (ignoring default arguments and variadics) and for each \(i\) there is an implicit conversion sequence \(\mathrm{ICS}_i\) from the type of \(a_i\) to the type of \(p_i\). Each standard conversion sequence has a rank: \(\mathsf{Exact} < \mathsf{Promotion} < \mathsf{Conversion}\) (smaller is better); a user-defined conversion ranks below every standard one, and an ellipsis match below that.
Definition 5.4.2 (Better viable function)
Viable \(F\) is better than viable \(G\), written \(F \succ G\), if \(\mathrm{ICS}_i(F)\) is not worse than \(\mathrm{ICS}_i(G)\) for every argument \(i\), and strictly better for at least one \(i\). (C++ adds tie-breakers after this rule — non-template over template, more specialized template — which we omit.) The best viable function is the viable \(F\) with \(F \succ G\) for every other viable \(G\); if no such \(F\) exists the call is ambiguous, and if no candidate is viable it is unresolved.
Algorithm 5.4.3 (Best viable function: one tournament pass plus a verification pass)
- Input: the viable functions \(V = \langle F_1, \dots, F_r \rangle\) with their conversion ranks.
- Output: the best viable function, or "ambiguous", or "no viable function".
- Precondition: each per-argument comparison of conversion sequences is a strict weak order.
- Postcondition: the returned \(F\) satisfies Definition 5.4.2; "ambiguous" is returned only if no viable function beats all others.
- Invariant: after examining \(F_1, \dots, F_j\), if a best viable function exists among them it is \(\mathit{best}\).
Argument-dependent lookup¶
Definition 5.4.4 (Associated namespaces and ADL)
For a type \(T\), its associated namespaces \(\mathrm{assoc}(T)\) are: for a fundamental type, none; for a class type, the innermost namespace enclosing the class (and those of its base classes and, for a class template specialization, of its template arguments); for a pointer or array type, those of the element type; for a function type, those of its parameter and return types. For an unqualified call \(f(a_1, \dots, a_k)\), let \(X\) be the result of ordinary unqualified lookup of \(f\). If \(X\) contains a class member, a block-scope function declaration, or something that is not a function, the candidate set is \(X\). Otherwise it is \(X \cup \bigcup_i \{\, \text{functions named } f \text{ declared in } N \mid N \in \mathrm{assoc}(\mathrm{type}(a_i)) \,\}\). A parenthesized callee (f)(…) is not an unqualified call, so ADL does not apply.
Algorithm 5.4.5 (Candidate set with ADL)
- Input: an unqualified call \(f(a_1, \dots, a_k)\), its scope, the argument types.
- Output: the candidate set of Definition 5.4.4.
- Precondition: ordinary lookup is available (Lessons 5.1–5.3).
- Postcondition: the set is exactly Definition 5.4.4's.
- Invariant:
seenholds the namespaces already searched, so each is searched once.
function Candidates(f, args, scope):
X ← OrdinaryLookup(f, scope)
if the callee was parenthesized or X has a member / block-scope decl / non-function:
return X
seen ← ∅; C ← X
for a in args:
for N in Assoc(type(a)): # Definition 5.4.4, recursively
if N ∉ seen: seen ← seen ∪ {N}; C ← C ∪ FunctionsNamed(f, N)
return C
Macro hygiene¶
Definition 5.4.6 (Syntax context and hygienic binding)
Every identifier occurrence carries a syntax context \(c\): the empty context for text the user wrote, and for text introduced by a macro expansion, a fresh mark for that expansion step added to the context. A binding occurrence \((n, c_b)\) binds a use \((n, c_u)\) in its scope only if \(c_u = c_b\) (for local variables; items and paths use more permissive rules). Expansion is hygienic if a use written by the user can never be bound by a binder introduced by a macro, and a use introduced by a macro can never be bound by a binder written by the user at the call site.
3. Worked example¶
Overload resolution¶
The declarations and calls of the Clang box in §7:
void f(int, double); // F1
void f(double, int); // F2
void f(long); // F3
f(1, 2); f(1.0, 2); f('a');
| call | candidate | viable? | \(\mathrm{ICS}_1\) | \(\mathrm{ICS}_2\) |
|---|---|---|---|---|
f(1, 2) |
F1 | yes | int→int: Exact | int→double: Conversion |
| F2 | yes | int→double: Conversion | int→int: Exact | |
| F3 | no (2 arguments, 1 parameter) | |||
f(1.0, 2) |
F1 | yes | double→int: Conversion | int→double: Conversion |
| F2 | yes | double→double: Exact | int→int: Exact | |
f('a') |
F3 | yes | char→long: Conversion (an integral conversion, not a promotion: char promotes only to int) |
Algorithm 5.4.3:
| call | step | best | comparison | result |
|---|---|---|---|---|
f(1, 2) |
start | F1 | ||
| \(j = 2\) | F1 | F2 ≻ F1? argument 1 worse (Conversion vs Exact): no | best stays F1 | |
| verify | F1 | F1 ≻ F2? argument 2 worse: no | ambiguous | |
f(1.0, 2) |
start | F1 | ||
| \(j = 2\) | F2 | F2 ≻ F1: both arguments strictly better | best ← F2 | |
| verify | F2 | F2 ≻ F1: yes | F2 | |
f('a') |
start | F3 | only one viable | F3 |
Argument-dependent lookup¶
namespace geo { struct Point { int x, y; }; int norm1(Point p); }
int a(geo::Point p) { return norm1(p); } // ADL
int b(geo::Point p) { return (norm1)(p); } // no ADL
| call | ordinary lookup \(X\) | ADL applies? | \(\mathrm{assoc}(\texttt{geo::Point})\) | candidates | result |
|---|---|---|---|---|---|
norm1(p) in a |
∅ (global scope has no norm1) |
yes (unqualified, \(X\) empty) | {geo} | {geo::norm1} | geo::norm1 |
(norm1)(p) in b |
∅ | no (parenthesized) | — | ∅ | error: undeclared norm1 (Clang suggests geo::norm1, §7) |
Macro hygiene¶
The Rust program of the §7 box: let tmp = 1; println!("{}", double!(tmp + 1)); with double!($e) => { let tmp = $e; tmp + tmp }.
| identifier | written by | context | binds to |
|---|---|---|---|
tmp in let tmp = 1 |
user | \(c_0\) | — (a binder) |
tmp in the argument tmp + 1 |
user | \(c_0\) | the user's let tmp = 1 |
tmp in let tmp = $e |
macro, expansion 1 | \(c_0 \cdot m_1\) | — (a binder) |
tmp in tmp + tmp |
macro, expansion 1 | \(c_0 \cdot m_1\) | the macro's let tmp |
The argument tmp + 1 evaluates to 2 (its tmp has context \(c_0\) and cannot see the macro's binder), and the macro doubles it: 4. The C preprocessor has no contexts, so its BUMP() in §7 increments whichever count is visible where it is pasted.
Try it
Change f('a') to f(1L) and f(1, 2) to f(1, 2.0) and predict Clang's answers with Algorithm 5.4.3 before compiling. No drill covers this lesson; the quiz items overload-ics-table and adl-candidates are computations of this kind.
4. Invariants and correctness¶
Overload resolution¶
Lemma 5.4.7 (Better is a strict partial order)
If each per-argument comparison of conversion sequences is a strict weak order, then \(\succ\) of Definition 5.4.2 is irreflexive and transitive.
Proof
Irreflexive: \(F \succ F\) would need an argument where \(\mathrm{ICS}_i(F)\) is strictly better than itself. Transitive: let \(F \succ G\) and \(G \succ H\). For every \(i\), \(\mathrm{ICS}_i(F)\) is not worse than \(\mathrm{ICS}_i(G)\), which is not worse than \(\mathrm{ICS}_i(H)\); in a strict weak order "not worse" is transitive, so \(\mathrm{ICS}_i(F)\) is not worse than \(\mathrm{ICS}_i(H)\). At the argument \(j\) where \(F\) is strictly better than \(G\): strictly better followed by not worse is strictly better (again by the weak order), so \(F\) is strictly better than \(H\) at \(j\). Hence \(F \succ H\).
Theorem 5.4.8 (The tournament finds the best viable function)
Algorithm 5.4.3 returns \(F\) if and only if \(F\) is the best viable function of Definition 5.4.2, and "ambiguous" if and only if viable functions exist but none is best.
Proof
Uniqueness first: if \(F\) and \(F'\) were both best, \(F \succ F'\) and \(F' \succ F\), so \(F \succ F\) by transitivity, contradicting irreflexivity (Lemma 5.4.7). Loop invariant: suppose a best viable function \(F^{*}\) exists among \(F_1, \dots, F_j\). If \(F^{*} = F_j\), then \(F_j \succ \mathit{best}\) (it beats everything else), so the loop sets \(\mathit{best} \leftarrow F_j\). If \(F^{*}\) came earlier, by induction \(\mathit{best} = F^{*}\) before step \(j\), and \(F_j \succ F^{*}\) is impossible (it would give \(F^{*} \succ F^{*}\) by transitivity with \(F^{*} \succ F_j\)), so \(\mathit{best}\) stays \(F^{*}\). At the end, if a best function exists it is \(\mathit{best}\), and the verification pass succeeds. Conversely, the verification pass returns \(\mathit{best}\) only if it beats every other viable function, which is the definition of best. If none is best, verification fails for whatever \(\mathit{best}\) is, and the result is "ambiguous".
Argument-dependent lookup¶
Proposition 5.4.9 (ADL only adds candidates, and only functions)
The candidate set of Algorithm 5.4.5 contains the ordinary lookup result \(X\); every extra element is a function named \(f\) declared in a namespace associated with some argument type; and ADL is not applied to parenthesized callees or when \(X\) contains a member, a block-scope declaration or a non-function.
Proof
Read off the algorithm: \(C\) starts as \(X\) and only grows by FunctionsNamed(f, N) for \(N \in \mathrm{Assoc}(\cdot)\); the early return covers the excluded cases. seen makes each namespace contribute once, so the loop terminates after at most \(\sum_i \lvert \mathrm{assoc}(\mathrm{type}(a_i)) \rvert\) namespace lookups. Consequence for correctness arguments: a program that compiled without ADL (all calls resolved by \(X\)) can change meaning when ADL is enabled only if a new candidate from an associated namespace is better by Theorem 5.4.8 — which is exactly the "ADL hijacking" hazard that the parenthesized form turns off.
Macro hygiene¶
Theorem 5.4.10 (Context-keyed binding is hygienic)
If every expansion step adds a mark not used by any other step, and a binder binds a use only when their contexts are equal (Definition 5.4.6), then no user-written use is bound by a macro-introduced binder and no macro-introduced use is bound by a user-written binder at the use site.
Proof sketch (full proof and the lazy-mark algorithm: [KFFD86, §3]; [DHB92, §3]; Lesson 4.8, Theorem 4.8.10)
A user-written use has a context without the mark \(m\) of the expansion step that introduced a macro binder, while that binder's context contains \(m\); the contexts differ, so the binder does not bind the use. Symmetrically, a macro-introduced use carries \(m\), and a user-written binder at the use site does not. Identifiers the macro receives as arguments are pasted unchanged, so they keep the user's context and bind as the user intended. Real systems relax "equal contexts" for items and paths (Rust's macro_rules! uses mixed-site hygiene: local variables and labels are hygienic, items resolve at the use site), which is why the theorem is stated for local bindings.
5. Complexity¶
Variables: \(c\) candidates, \(k\) arguments, \(r \le c\) viable functions, \(t\) the size of an argument type (number of nested types in it), \(N\) the number of tokens produced by macro expansion.
| Technique | Time | Space | Pathological input |
|---|---|---|---|
| Overload resolution | \(O(c\, k)\) to build conversion sequences + \(O(r\, k)\) for Algorithm 5.4.3 | \(O(c\, k)\) | templates: each candidate needs deduction; Swift: operator overloads multiply across an expression (exponential in the worst case; Ch 7) |
| ADL | \(O(\sum_i t_i)\) namespaces to collect + one lookup each | \(O(\sum_i t_i)\) | deeply nested template arguments (vector<pair<A, map<B, C>>>): every argument type's namespaces are associated |
| Hygiene | \(O(N)\) with lazily propagated marks; \(O(N \cdot d)\) with eager renaming at expansion depth \(d\) | a context per identifier | deeply nested macros with eager renaming |
Justification. Algorithm 5.4.3 makes \(r - 1\) comparisons in the loop and at most \(r - 1\) in verification, each \(O(k)\); the naive "compare all pairs" alternative is \(O(r^2 k)\). Clang does the same one-pass-plus-verify (OverloadCandidateSet::BestViableFunction). ADL visits each associated namespace once thanks to seen. Hygiene costs are Lesson 4.8's.
6. Variants and refinements¶
Overload resolution¶
- C++ tie-breakers: non-template beats template, more specialized template beats less, constraints (C++20 concepts) order candidates by subsumption.
- Swift's solution ranking: the solver finds complete solutions (every overload in an expression chosen), then compares their scores (counts of non-default literals, value-to-optional conversions, fixes, …) and finally the declarations by "more specialized" [SWIFT-TypeChecker]; overloading is resolved jointly with type inference rather than per call.
- Overloading without implicit conversions (Rust traits, Haskell type classes): the choice is made by the types exactly, so ranking disappears; ambiguity becomes "type annotations needed".
Argument-dependent lookup¶
- Hidden friends: C++ functions declared only as friends inside a class are findable only by ADL, which keeps overload sets small (a standard library technique).
- The two-step
using std::swap; swap(a, b);: ordinary lookup findsstd::swap, ADL adds the type's ownswap, and overload resolution picks the more specific. - Languages without ADL resolve operators through the operand's type directly (Rust
impl Add for T, Swift operators as static members): the same effect, no surprise candidates.
Macro hygiene¶
- Marks and substitutions [DHB92] vs sets of scopes [Fla16] (Racket), vs rustc's mixed-site hygiene with
$cratefor paths (Lesson 4.8 §6). - Unhygienic by default with gensym (Common Lisp, Clojure's
foo#): the macro author must request fresh names.
7. In real compilers¶
Overload resolution¶
Clang: OverloadCandidateSet::BestViableFunction and isBetterOverloadCandidate (clang/lib/Sema/SemaOverload.cpp, LLVM 23.1.2 [CLANG-SemaOverload]) implement Algorithm 5.4.3 and the C++ tie-breakers. GCC: joust compares two candidates and build_new_function_call drives the tournament (gcc/cp/call.cc, gcc-15 [GCC-CPCall]). Swift: compareSolutions ranks complete solutions (lib/Sema/CSRanking.cpp, swift-6.1-RELEASE [SWIFT-CSRanking]); swiftc is not available in this container, so the Swift side is covered by the source pointer and [SWIFT-TypeChecker].
Clang 23's ambiguous-call diagnostic lists the tied candidates
Reproduce (clang 23.1.2):
cat > overload.cpp <<'EOF'
void f(int, double);
void f(double, int);
void f(long);
void g() {
f(1, 2); // both two-argument candidates need one conversion: ambiguous
f(1.0, 2); // f(double, int) is an exact match
f('a'); // f(long): integral conversion
}
EOF
clang++-23 -fsyntax-only overload.cpp
Output (complete):
overload.cpp:5:3: error: call to 'f' is ambiguous
5 | f(1, 2); // both two-argument candidates need one conversion: ambiguous
| ^
overload.cpp:1:6: note: candidate function
1 | void f(int, double);
| ^
overload.cpp:2:6: note: candidate function
2 | void f(double, int);
| ^
1 error generated.
What to notice: exactly the verdicts of the §3 table: f(1, 2) is ambiguous between F1 and F2 (each is better on one argument), the other two calls resolve silently. The notes list only the viable candidates that tied — F3 was not viable (Definition 5.4.1).
Argument-dependent lookup¶
Clang: Sema::ArgumentDependentLookup and the computation of associated namespaces and classes (clang/lib/Sema/SemaLookup.cpp, LLVM 23.1.2 [CLANG-SemaLookup]). GCC: lookup_arg_dependent (gcc/cp/name-lookup.cc, gcc-15 [GCC-NameLookup]).
ADL finds geo::norm1; parentheses switch it off (Clang 23)
Reproduce (clang 23.1.2):
cat > adl.cpp <<'EOF'
namespace geo {
struct Point { int x, y; };
int norm1(Point p) { return (p.x < 0 ? -p.x : p.x) + (p.y < 0 ? -p.y : p.y); }
}
int a(geo::Point p) { return norm1(p); } // found by argument-dependent lookup
int b(geo::Point p) { return (norm1)(p); } // parentheses suppress ADL
EOF
clang++-23 -fsyntax-only adl.cpp
Output (complete):
adl.cpp:6:31: error: use of undeclared identifier 'norm1'; did you mean 'geo::norm1'?
6 | int b(geo::Point p) { return (norm1)(p); } // parentheses suppress ADL
| ^~~~~
| geo::norm1
adl.cpp:3:7: note: 'geo::norm1' declared here
3 | int norm1(Point p) { return (p.x < 0 ? -p.x : p.x) + (p.y < 0 ? -p.y : p.y); }
| ^
1 error generated.
What to notice: line 5 compiles although no norm1 is visible there — ADL added geo::norm1 from the namespace associated with geo::Point (Definition 5.4.4). Line 6 is the same call with a parenthesized callee, so only ordinary lookup runs and fails; Clang's typo correction (Lesson 5.8) then searches other namespaces and suggests the qualified name.
Macro hygiene¶
rustc: every span carries a SyntaxContext (compiler/rustc_span/src/hygiene.rs [RUSTC-Hygiene]), macro_rules! expansion creates fresh marks in compiler/rustc_expand/src/mbe/transcribe.rs [RUSTC-MBE], and local-variable resolution compares identifiers by name and context. Clang's preprocessor has no contexts (clang/lib/Lex): Lesson 4.8 traces its expansion.
Hygienic macro_rules! in rustc 1.94 vs capture in the C preprocessor (Clang 23)
Reproduce (rustc 1.94.1, clang 23.1.2):
cat > hyg.rs <<'EOF'
macro_rules! double {
($e:expr) => {{ let tmp = $e; tmp + tmp }};
}
macro_rules! set_tmp {
() => { let tmp = 100; };
}
fn main() {
let tmp = 1;
println!("{}", double!(tmp + 1)); // the macro's tmp does not capture ours
set_tmp!();
println!("{}", tmp); // still 1: the macro's `let tmp` is invisible here
}
EOF
rustc --edition 2021 -A unused_variables hyg.rs -o hyg && ./hyg
cat > hyg.c <<'EOF'
#include <stdio.h>
int count = 0; /* the macro's author means this one */
#define BUMP() (count++)
int f(void) { int count = 10; BUMP(); return count; }
int main(void) { int r = f(); printf("f() = %d, global count = %d\n", r, count); }
EOF
clang-23 -E -P hyg.c | tail -2
clang-23 hyg.c -o hygc && ./hygc
Output (complete; the Rust program, the preprocessed tail, the C program):
4
1
int f(void) { int count = 10; (count++); return count; }
int main(void) { int r = f(); printf("f() = %d, global count = %d\n", r, count); }
f() = 11, global count = 0
What to notice: Rust prints 4 (the argument's tmp is the user's, context \(c_0\)) and then 1 (the macro's let tmp = 100 has context \(c_0 \cdot m\) and cannot bind the user's later tmp): Theorem 5.4.10 in both directions. In C, BUMP() is pasted as tokens and its count resolves in f's scope to the local, so the global the author meant is never incremented — capture by the use site.
8. Comparison¶
| Technique | Power / precision | Speed (asymptotic · practical) | Output / error quality | Implementation effort | Typical use |
|---|---|---|---|---|---|
| Overload resolution | One name, many functions, chosen by argument types; unique best or an ambiguity error (Theorem 5.4.8) | \(O(c\,k)\) per call · templates and Swift's joint solving can be expensive | Ambiguity errors list the tied candidates; "no viable function" lists why each failed | High (conversion ranking, templates, tie-breakers) | C++, Swift, Java, C#, Ada |
| Argument-dependent lookup | Adds functions from the arguments' namespaces (Proposition 5.4.9); makes operators and customization points work | \(O(\sum t_i)\) namespace lookups per unqualified call | Surprising candidates ("ADL hijacking"); errors mention functions the reader cannot see nearby | Medium | C++ only (other languages attach operators to types) |
| Macro hygiene | Binding by (name, context): no accidental capture (Theorem 5.4.10) | \(O(N)\) with lazy marks | Errors point into expansions with a backtrace | Medium–high (contexts on every identifier) | Rust, Scheme, Racket; not C or C++ |
Choose overloading when the language has implicit conversions or many numeric types and you accept a ranking specification; choose trait- or type-class-based dispatch when you want exact matching and inference instead. Choose ADL only in a language with free-function operators and namespaces (C++); newer designs attach operations to types. Choose hygiene for any macro system that manipulates code rather than text.
9. Assessment¶
| Technique | Quiz ids | Drill | Flashcard tag | Exercises |
|---|---|---|---|---|
| Overload resolution | overload-ics-table, find-clang-bestviable |
— (the §3 table format; quiz computations) | overload-resolution |
— |
| Argument-dependent lookup | adl-candidates, adl-parens |
— | adl |
— |
| Macro hygiene | hygiene-contexts-rust, cpp-capture |
— (Lesson 4.8's quiz items too) | hygiene |
— |
Pebble has no overloading, namespaces or macros, so no exercise implements this lesson; its outcomes are checked by the quiz and the flashcards, and the runs above are reproducible.
References¶
See the chapter references.