Concepts / Function Declarations and Definitions

Function Declarations and Definitions

Every function call is a small handoff.

  • Programming

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?

  • Both compile

  • The call with 2 compiles; the call with a string literal is a compile-time error

  • The call with 2 is a runtime error; the string literal call compiles

Reveal answer
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.

  1. 1
    The call sqrt(2) supplies the argument 2, which has type int at this point.
  2. 2
    The compiler compares the argument type int with the parameter type double.
  3. 3
    The types differ, so the compiler looks for an implicit argument type conversion from int to double.
  4. 4
    The conversion is available, so the compiler performs it. The argument now behaves like double{2}.
  5. 5
    The parameter is initialized from that double value, and the function receives 2 as a double.
Call sqrt(2)argument has type intCompare typesint vs doubleImplicit conversionint becomes doubleParameter initializedcopy-initialization ofdoubleFunction receives 2as a doubleargument suppliedtypes differconverted valueparameter holds value
What steps happen from the moment a call supplies an argument to the moment the parameter holds its value, and where does an implicit conversion fit in?

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.

cpp
Output
The first line compiles. The second line stops the build with a compile-time error.
Notes on the code
  • 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.
Key point

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.

Call is writtenargument and parametertypes knownExact type match?argument type vs parametertypeImplicit conversionavailable?e.g. int to doubleCall compilesargument used directlyCall compilesafter implicit conversionCompile-time errorno valid path to parametercheckyesnoyesno
Given an argument type and a parameter type, what decision path determines whether the call compiles: exact match, implicit conversion available, or error?
Worked example

Predicting two calls to sqrt

For each call, decide whether it compiles: sqrt(2) and sqrt("three"). The parameter type is double.

  1. 1
    Identify the argument type of sqrt(2)

    The literal 2 has type int at the call site.

  2. 2
    Compare with the parameter type

    The parameter is double, so the types do not match exactly.

  3. 3
    Look for an implicit conversion

    An implicit conversion from int to double is available, so the compiler applies it.

  4. 4
    Identify the argument type of sqrt("three")

    The argument is a string literal, which does not match double.

  5. 5
    Look for an implicit conversion for the string

    There is no implicit conversion from a string to double, so there is no valid path.

Result

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 like

    Writing sqrt("three") and expecting the error to appear only when sqrt runs.

    Why it fails

    The checking happens at the call site, against the declared parameter type. The function body is not consulted.

    Do this instead

    Read 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 like

    Assuming any value that looks like a number is accepted by a double parameter.

    Why it fails

    Acceptance depends on the argument type and whether an implicit conversion exists, not on how the value looks.

    Do this instead

    Identify 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.

cpp
Notes on the code
  • 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.
doublereturn type: part of thetypegetfunction name: not part ofthe typeconst vector ofdouble referenceargument type: part of thetypevecparameter name: not part ofthe typeintargument type: part of thetypeindexparameter name: not part ofthe type
Which parts of a function declaration (return type, parameter types, parameter names) are part of its type, and which are not?

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.

AspectDeclarationDefinition
Gives name, return type, parameter typesYesYes
Provides the function itselfNot requiredYes
Argument names used by the compilerNo, ignored unless also a definitionYes, when the declaration is also a definition
Contributes to the function typeReturn and argument typesReturn and argument types

What a declaration and a definition provide to the compiler

Practice Predicting Calls

MEDIUM

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.

Related concepts

More concepts starting with F