Function Declarations and Definitions
Every function call is a small handoff.
A Handoff at the Call Site
Every function call is a small handoff. The caller supplies values, and the function receives them through its parameters. Most of the time this handoff works without any thought. Sometimes, though, the value you supply does not match the type the function expects, and you need to know what the compiler does at that moment and why a mismatch is caught before the program ever runs.
The goal is to build a habit. When you read a call, you should be able to say what value reaches the parameter, what type that value has at that moment, and whether the compiler will accept it. The running example in this article is the library function sqrt, which takes a double and returns the square root of that double.
What do you think happens?
Two calls are made to sqrt. One passes the integer literal 2. The other passes a string literal. Which one compiles, and what kind of problem does the other cause?
Reveal answer
The call with 2 compiles; the call with a string literal is a compile-time error.
The integer 2 can be implicitly converted to double, so the argument is accepted. A string literal has no implicit conversion to double, so the compiler reports an error and the program is not built.
Tracing One Value Into a Parameter
Follow the first call, sqrt(2), one step at a time. The caller writes the literal 2. The parameter of sqrt is a double, so the value needs to be a double before it can be stored there. Nothing about the call itself changes the text you wrote, but the state of the value does change on the way to the parameter.
- 1The call sqrt(2) supplies the argument 2, which has type int at this point.
- 2The compiler compares the argument type int with the parameter type double.
- 3The types differ, so the compiler looks for an implicit argument type conversion from int to double.
- 4The conversion is available, so the compiler performs it. The argument now behaves like double{2}.
- 5The parameter is initialized from that double value, and the function receives 2 as a double.
Argument Passing as Copy Initialization
The rule that governs argument passing is short: the semantics of argument passing are identical to the semantics of copy initialization. The way a value is handed to a parameter follows the same rules as the way a value is used to initialize a new variable by copy. Whenever you have a feel for how a variable is initialized from an expression, you already have a feel for how a parameter is initialized from an argument.
Two things happen under that rule. First, the argument's type is checked against the parameter's type. Second, if the types differ and an implicit argument type conversion is possible, the compiler performs that conversion when it is necessary. Nothing in this process requires you to write the conversion yourself. The programmer writes the literal 2, and the compiler supplies the double form the parameter needs.
The first line compiles. The second line stops the build with a compile-time error.- sqrt takes one double parameter, so each argument is copy-initialized into that parameter.
- The int literal 2 is implicitly converted to double before the function receives it.
- The string literal has no implicit conversion to double, so the error is reported when the call is compiled.
Argument passing is copy initialization. The argument is checked against the parameter type, and an implicit conversion is applied only when the types differ and a conversion exists.
Predicting Whether a Call Compiles
Now trace the second call. A string literal cannot be repaired by conversion, because there is no implicit conversion from a string to double. The decision path is short: check whether the argument type matches the parameter type, then check whether an implicit conversion exists, and only if neither holds is the call an error.
Predicting two calls to sqrt
For each call, decide whether it compiles: sqrt(2) and sqrt("three"). The parameter type is double.
- 1Identify the argument type of sqrt(2)
The literal 2 has type int at the call site.
- 2Compare with the parameter type
The parameter is double, so the types do not match exactly.
- 3Look for an implicit conversion
An implicit conversion from int to double is available, so the compiler applies it.
- 4Identify the argument type of sqrt("three")
The argument is a string literal, which does not match double.
- 5Look for an implicit conversion for the string
There is no implicit conversion from a string to double, so there is no valid path.
sqrt(2) compiles because the int is converted to double. sqrt("three") is a compile-time error reported at the line where the call is written.
Mistakes Beginners Make with Argument Types
Common mistake Assuming the function body decides whether a call is legal
What it looks likeWriting sqrt("three") and expecting the error to appear only when sqrt runs.
Why it failsThe checking happens at the call site, against the declared parameter type. The function body is not consulted.
Do this insteadRead the declared parameter type and check the argument against it at the line where the call is written.
Common mistake Believing every argument that looks numeric will be accepted
What it looks likeAssuming any value that looks like a number is accepted by a double parameter.
Why it failsAcceptance depends on the argument type and whether an implicit conversion exists, not on how the value looks.
Do this insteadIdentify the argument's type first, then check for an exact match or an available implicit conversion.
Common mistake Expecting a mismatch to be found only at runtime. Mismatches that cannot be converted are caught when the program is compiled.
What Makes Up a Function's Type
A function declaration tells the compiler the name, return type, and parameter types of a function. A function's type is its return type together with its argument types. Parameter names are not part of that type. Take the declaration of get. Its return type is double, and its argument types are a reference to a const vector of double and an int. The names vec and index are not part of the type, so the type reads as double taking a const vector-of-double reference and an int.
- double is the return type and is part of the function type.
- const std::vector<double>& and int are the argument types and are part of the function type.
- vec and index are parameter names and are not part of the function type.
Argument Names and Class Members
Argument names in a declaration help a human reader understand what each parameter means, but the compiler ignores them unless the declaration is also a function definition. Changing vec to values in a declaration does not change how calls are matched or what the function's type is. This is why a declaration can be read as a description of how to call the function, with the names serving only as documentation.
For class member functions, the name of the class is also part of the function type. The source gives the member operator[] of String, which returns a char reference and takes an int. Its type records the class name along with the return and argument types. So a member function's type is richer than a free function's: the class it belongs to is included.
Declarations Versus Definitions
A declaration introduces a function to the compiler by giving its name, return type, and parameter types. A definition is the declaration that also provides the function itself. The source's rule about names depends on this difference: argument names in a declaration are ignored by the compiler unless the declaration is also a definition.
| Aspect | Declaration | Definition |
|---|---|---|
| Gives name, return type, parameter types | Yes | Yes |
| Provides the function itself | Not required | Yes |
| Argument names used by the compiler | No, ignored unless also a definition | Yes, when the declaration is also a definition |
| Contributes to the function type | Return and argument types | Return and argument types |
What a declaration and a definition provide to the compiler
Practice Predicting Calls
Given a function that takes one double parameter, predict whether each of these calls compiles, and name the type conversion or error that applies: sqrt(9), sqrt("nine"). Then state which parts of the declaration double get(const std::vector<double>& vec, int index) form its type.
Hints
- Identify the argument type first, then compare it with double.
- If the types differ, ask whether an implicit conversion exists before deciding it is an error.
- Parameter names such as vec and index are not part of the type.
Key Takeaways
- Argument passing follows the semantics of copy initialization: the argument is checked against the parameter type, and an implicit conversion is applied when necessary.
- A call such as sqrt(2) compiles because the int argument converts implicitly to double, while sqrt("three") fails at compile time because no conversion from string to double exists.
- Type checking happens at the call site against the declared parameter type, so a bad call is reported on the line where it is written.
- A function's type is its return type together with its argument types; argument names are ignored unless the declaration is also a definition, and for class members the class name is part of the type.