C++ Part I · Module 6 · Functions
Declarations, Definitions and Why Order Matters
Module 5 ended on the point this module answers: a program written as one long main has every name visible to every line, every piece of logic reachable from every other, and nothing that can be tested on its own. A function fixes that, and the reason is not mainly that it saves you from repeating code. It is that a function is a contract — this goes in, that comes out — which means a reader can use it without reading it, and the compiler can check every use of it against the contract rather than against the implementation. This chapter is about writing that contract down separately from fulfilling it. C++ lets you declare a function without defining it, and that separation is what makes headers work, what lets two functions call each other, and what decides which stage of the build catches your mistakes. The chapter's sharpest finding is about that last point. There is a very ordinary typo that the compiler will accept in complete silence and the linker will reject with a message naming a function you never wrote, and the reason is a feature of the language that does not arrive properly until Chapter 6.7.
Foundation16 min readC++Part IFundamentalsFunctions
Part I · Module 6 · Chapter 6.1 · Declarations, Definitions and Why Order Matters
1. Two Different Statements About a Function
C++ separates two things that look like one:
int twice(int n); // a DECLARATION
int twice(int n) { return 2 * n; } // a DEFINITIONA declaration states that a function exists, what it is called, what it takes and what it returns. It is a promise. It ends in a semicolon and has no body.
A definition supplies the body. Every definition is also a declaration — it states all the same things and then adds the implementation.
The declaration is the contract. Everything a caller needs is in it, and nothing a caller does not need is in it:
| From the declaration a caller learns | |
|---|---|
| the name | twice |
| what to pass | one int |
| what comes back | an int |
What the caller does not learn is how the result is computed, and that is the point. Chapter 6.9 will make the design argument; this chapter establishes the mechanism.
A declaration is what the compiler checks your calls against. A definition is what the linker needs to make the program run. Those are different jobs, done at different stages, and Module 2 already taught the stages.
2. Declare Before You Use
The compiler reads a translation unit from top to bottom, and a name must be declared before it is used. Call a function that has not been declared yet and the compile fails:
std::cout << twice(4) << '\n'; // no declaration in scope yet
// ...
static int twice(int n) { return 2 * n; }error: use of undeclared identifier 'twice'The definition is in the file, below. That does not help: at the point of the call the compiler has not read it yet and has nothing to check the call against.
Two ways to fix it. Move the definition above the call, or — better — declare the function at the top and define it wherever you like:
#include <iostream>
static int twice(int n); // declaration: tells the compiler it exists
int main()
{
std::cout << twice(4) << '\n';
return 0;
}
static int twice(int n) { return 2 * n; } // definition: supplies the bodyExpected output:
8The second form is what you will see in real code, for three reasons.
main can go first. A reader meeting the file sees what the program does before how.
Two functions can call each other. With definitions alone, whichever comes first cannot see the other. Declarations break the cycle.
It is what a header is. Chapter 2.4 established that a header holds declarations and a source file holds definitions. This chapter is the reason that split exists: the declarations are what other translation units need, and the definitions are what they must not have.
3. A Note on static in These Examples
Every function in this chapter's examples is marked static. That keeps each one private to its own translation unit, which is why a file full of small examples does not collide with anything.
That is Chapter 7.5's subject and nothing here depends on understanding it. Read static as "not visible outside this file" and move on; Chapter 7.5 explains what it actually controls and when you want it.
4. The Compiler Checks the Call Against the Declaration
Once a declaration is visible, every call is checked against it. Arguments of the wrong type are converted if a conversion exists, or rejected if none does, and the number of arguments must match.
This is the real value of declaring before use. Without a declaration there is nothing to check against, and a language that allows it — C did, historically — defers every such mistake to run time.
Chapter 4.10's conversions apply here, which means a call can be accepted and still not do what you meant:
static int twice(int n);
// ...
twice(3.7); // accepted: 3.7 converts to int, so n is 3Measured, that one warns, because the argument is a literal and the compiler can see the value being damaged:
narrow.cpp:3:33: warning: implicit conversion from 'double' to 'int'
changes value from 3.7 to 3 [-Wliteral-conversion]Output: 6, confirming n was 3. Pass the same value in a variable and the warning goes away:
double measured = 3.7;
twice(measured); // zero warnings; n is still 3Measured: zero diagnostics, output 6. This is exactly Chapter 4.10 §7's pattern — the compiler warns when it can see a specific value being damaged and is silent when the damage depends on a value it does not know — now crossing a function boundary. Chapter 3.7 §4 measured the same split at initialization.
One small consequence of that chapter worth knowing: braces carry their strictness across the call. twice({measured}) is a compile error, type 'double' cannot be narrowed to 'int' in initializer list, which is Chapter 3.7 §8's rule reaching an argument. It also draws -Wbraced-scalar-init, so treat it as evidence that the rule is general rather than as a style to adopt.
Chapter 6.7 is where the choice between candidate functions is made, and it is where this gets genuinely surprising.
5. A Declaration Is a Promise the Linker Holds You To
Declaring a function and never defining it is allowed, and whether that is a problem depends entirely on whether anybody calls it.
Declared and never called — fine:
int twice(int n); // promised, never used
int main() { return 0; }Measured: compiles, links, exits 0, with zero diagnostics. The promise was never collected on.
Declared, called, never defined — fails, and at the linker:
int twice(int n);
int main() { return twice(4); }$ c++ -std=c++17 -Wall -Wextra -Wpedantic -c promise.cpp -o promise.o
$ ← compiles cleanly
$ c++ promise.o -o promise
Undefined symbols for architecture arm64:
"twice(int)", referenced from:
_main in promise.o
ld: symbol(s) not found for architecture arm64
clang++: error: linker command failed with exit code 1You have seen this message before. Chapter 2.6 §6 produced exactly this shape — the same Undefined symbols heading, the same demangled name in quotes, the same referenced from line — when a translation unit used something defined in another file that was never linked in. This is the same mechanism meeting you from the function side.
And Chapter 2.6 §7's table already predicted it. Compiling can check "whether a call matches a visible declaration"; it cannot check whether the function exists anywhere. Linking catches "a name promised and never defined". The declaration satisfied the compiler completely, because satisfying the compiler is all a declaration is for.
The compiler checks that your call is consistent with the contract. The linker checks that somebody actually implemented it. Nothing checks both at once, and that is why the two stages produce different error messages for what feels like one mistake.
6. The Typo That the Compiler Will Not Catch
Here is the finding worth remembering from this chapter.
Get the return type wrong between declaration and definition and the compiler stops you:
int twice(int n); // declared returning int
double twice(int n) { return 2.0 * n; } // defined returning doubleerror: functions that differ only in their return type cannot be overloadedNow get a parameter type wrong instead:
#include <iostream>
int twice(int n); // declared taking int
int twice(double n) { return static_cast<int>(2 * n); } // defined taking double
int main() { std::cout << twice(4) << '\n'; return 0; }$ c++ -std=c++17 -Wall -Wextra -Wpedantic -c mismatch.cpp -o mismatch.o
$ ← zero diagnostics
$ c++ mismatch.o -o mismatch
Undefined symbols for architecture arm64:
"twice(int)", referenced from:
_main in mismatch.oNothing at compile time. Not a warning. And the linker's complaint is about twice(int) — the function in the declaration, which is the one twice(4) called and the one nobody defined.
The reason is that changing a parameter type does not redeclare the function. It declares a different function that happens to share the name. C++ permits several functions with one name as long as their parameters differ — that is overloading, and Chapter 6.7 is its chapter. So the file above honestly contains two functions: twice(int), declared and never defined, and twice(double), defined and never called. Both facts are individually fine. Together they are a program that does not link.
The return type behaves differently because C++ specifically forbids overloading on return type alone — there would be no way to tell from a call site which one you wanted. So that mistake is a redeclaration conflict, and the compiler can see it.
7. What to Actually Do
Declare before use, always — and prefer declaring at the top over reordering definitions.
Put the declaration in a header and include that header in the defining source file, so the two are checked against each other.
Let main come first in a small program; declarations above it cost two lines and buy readability.
Read a declaration as the contract. If it does not tell a caller what they need, the problem is the declaration, not the documentation.
Classify the error before debugging it. Chapter 2.7 §3's four questions apply: a compile error is about this file, a link error is about a missing or duplicated name.
Suspect a signature mismatch when the linker names a function you are sure you defined.
8. Mistakes Worth Making Once
Calling a function defined lower in the file. error: use of undeclared identifier — the definition below does not help.
Assuming a clean compile means the function exists. Measured: compiles cleanly, fails at link.
Mistyping a parameter type between declaration and definition. Measured: zero compile diagnostics, then a linker error naming the undefined one.
Expecting that to be a redeclaration error. It is an overload — Chapter 6.7.
Mistyping the return type and expecting the same silence. That one is caught: functions that differ only in their return type cannot be overloaded.
Not including your own header in your own source file. It is the one habit that turns the silent mismatch into a compile error.
Treating a demangled name in a linker error as noise. "twice(int)" is telling you precisely which signature was wanted.
9. What You Should Be Able to Say Now
- A declaration states that a function exists and what its contract is; a definition supplies the body. Every definition is also a declaration.
- The declaration is what a caller needs; the definition is what the linker needs.
- A name must be declared before use — measured:
error: use of undeclared identifier 'twice', even with the definition lower in the file. - Declaring at the top lets
maincome first, lets two functions call each other, and is exactly what a header is for. - Once a declaration is visible, every call is checked against it — including Chapter 4.10's conversions, so
twice(3.7)is accepted and passes 3. - Declared and never called links fine, with zero diagnostics.
- Declared, called, never defined compiles cleanly and fails at link with
Undefined symbols … "twice(int)" … referenced from _main. - That is the same message Chapter 2.6 §6 produced, and the same row of Chapter 2.6 §7's table — "a name promised and never defined".
- A wrong return type is a compile error:
functions that differ only in their return type cannot be overloaded. - A wrong parameter type is not an error at all at compile time — measured: zero diagnostics, then a link failure.
- The reason is that a different parameter type declares a different function, not a redeclaration — Chapter 6.7's overloading.
- By link time the types are in the mangled name, so
twice(int)andtwice(double)are two unrelated symbols. - Including your own header in your own source file is what makes that mismatch a compile error instead.
10. Exercises
Exercise 1 — declaration or definition? For each, say which it is and what a caller learns from it: (a) int twice(int n); (b) int twice(int n) { return 2 * n; } (c) void reset(); (d) static double mean(int a, int b) { return (a + b) / 2.0; }.
Exercise 2 — fix the order. A file calls twice(4) in main and defines twice below it. Give the two different fixes, and say which you would ship and why.
Exercise 3 — which stage fails? For each, say whether it fails at compile, at link, or not at all: (a) declared, called, defined (b) declared, called, not defined (c) declared, not called, not defined (d) defined, called, never declared separately (e) called before any declaration.
Exercise 4 — predict the diagnostic. Given int f(int);, say what happens for each definition and give the diagnostic where there is one: (a) int f(int n) {…} (b) double f(int n) {…} (c) int f(long n) {…} (d) int f(int n, int m) {…}.
Exercise 5 — read the linker error. You get Undefined symbols … "scale(int, double)", referenced from: _main. You are certain you defined scale. List the three things you would check, in order.
Exercise 6 — why the asymmetry? Explain why C++ makes a wrong return type a compile error but a wrong parameter type a link error. Your answer should mention what a call site can and cannot tell the compiler.
Exercise 7 — the habit. Write the three files — header, defining source, calling source — that make Exercise 4's case (c) a compile error rather than a link error, and say in one sentence what makes the difference.
11. Summary
- A declaration is the contract; a definition is the implementation. Every definition declares.
- Declare before use. Measured: a definition further down the file does not help.
- Declaring at the top lets
maincome first, permits mutual calls, and is what a header does. - Calls are checked against the declaration, conversions included.
- Declared and never called is fine; declared, called and never defined compiles clean and fails at link.
- That is Chapter 2.6's
Undefined symbolsmessage and its stage table, met from the function side. - A wrong return type is a compile error. A wrong parameter type produced zero compile diagnostics and a link failure.
- The reason: a different parameter list declares a different function — Chapter 6.7's overloading.
- Include your own header in your own source file and the mismatch becomes a compile error.
The declaration tells you a function takes an int. It does not tell you what happens to the int you hand over, and the next chapter is about that — the single detail the frozen curriculum calls out as load-bearing for everything in Modules 8, 12 and 13. The answer is one sentence long and the demonstration is one number: the address of the caller's object and the address of the parameter inside the function are not the same, which settles what a call actually does with what you give it.
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.
