Implicit Type Conversion in C++
Argument passing follows the semantics of copy initialization: the argument's type is checked against the parameter type, and an implicit conversion is applied when necessary.
A Function Call as a Handoff
Every function call is a small handoff. The caller supplies values, and the function receives them through its parameters. Usually this handoff happens without any thought, because the value you supply already has the type the function expects. Sometimes it does not match, and the compiler has to decide what to do. This article follows what the C++ compiler does at that handoff, and why a mismatch is caught before the program ever runs.
The goal is a habit. When you read a call, you should be able to say three things: which value reaches the parameter, what type that value has at that moment, and whether the compiler will accept it. The running example throughout this article is a single library function, sqrt, which takes a double and returns the square root of that double.
Predicting Two Calls to sqrt
Before reading the explanation, make a prediction about two calls to sqrt. The first passes the integer literal 2. The second passes a string literal. Decide which one you expect to compile, and what kind of problem you think the other one will cause.
What do you think happens?
Which of the two calls, sqrt(2) or sqrt with a string literal, compiles, and what happens to the other one?
Reveal answer
sqrt(2) compiles, and the string literal call is a compile-time error.
The integer 2 can be converted implicitly to the double the parameter needs. A string literal has no implicit conversion to double, so the compiler reports an error and the program is not built.
Argument Passing as Copy Initialization
Argument passing follows the semantics of copy initialization: the argument's type is checked against the parameter type, and an implicit conversion is applied when it is necessary.
The rule that governs argument passing is short. 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. In other words, if you already understand how a variable is initialized from a value of a different type, you already understand how a function parameter receives an argument.
Two things happen under that rule. First, the compiler checks the argument's type 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 compiler supplies it, and the parameter ends up as a new object of the parameter's type.
Tracing sqrt(2) Step by Step
The source illustrates the first call with sqrt(2). The comment in the code describes the call as passing the argument double{2}, which is the value 2 presented as a double. The programmer wrote an int-looking literal, and the compiler supplied the double form that the parameter needs.
Following the handoff for sqrt(2)
Trace what happens when sqrt is called with the integer literal 2.
- 1Identify the argument
The caller supplies the integer literal 2.
- 2Identify the parameter type
sqrt declares a parameter of type double.
- 3Check the types
The types differ: int against double.
- 4Look for an implicit conversion
An implicit conversion from int to double is available, so the compiler can perform it.
- 5Copy into the parameter
The converted value 2 is copy-initialized into a new double parameter object, which sqrt then uses.
The call compiles. The parameter receives the double value 2, and no conversion had to be written by the programmer.
Why sqrt with a String Fails
The second call in the source is the one that fails. A call to sqrt with a string literal cannot be repaired by conversion, because there is no implicit conversion from a string to double. The compiler reports an error, and the program does not get built. The diagram below follows the same decision that the compiler makes for any argument, and shows why the string branch ends in an error rather than a converted value.
| Call | Argument type | Implicit conversion to double? | Result |
|---|---|---|---|
| sqrt(2) | int | Yes | Compiles; the argument is converted and copied into the parameter |
| sqrt("three") | string literal | No | Compile-time error; the program is not built |
Two calls to sqrt, checked against the same double parameter
Where the Compiler Looks for the Check
The checking happens at the call site, against the declared parameter type. The body of the function is not consulted to decide whether the call is legal. This is why the diagnosis of a bad call points to the line where the call was written, rather than to some line inside the function.
When a call fails to compile, look first at the argument's type on that line and the declared parameter type in the function's declaration. The mismatch between those two is what the compiler is reporting.
Declarations, Names, and Function Types
A function declaration tells the compiler the name, return type, and parameter types of a function. It may also include argument names. Those names help a human reader understand what each parameter means, but the compiler does not use them, unless the declaration is also a function definition. The diagram below marks which parts of a declaration form the function's type.
A function's type is its return type together with its argument types. Names are not part of it. The source's example is get. Its return type is double, and its argument types are a reference to a const vector of double and an int. Its name, vec and index, is not part of the type. The type therefore reads as double taking a const vector-of-double reference and an int.
Mistakes Beginners Make with Argument Conversion
Common mistake Assuming that any value can be turned into the parameter type
What it looks likeCalling sqrt with a string literal and expecting the compiler to parse the text as a number.
Why it failsThe compiler only applies implicit conversions that exist. A string literal has no implicit conversion to double.
Do this insteadCheck whether the argument type has an implicit conversion to the parameter type. If it does not, convert the value explicitly in your own code before the call.
Common mistake Waiting for a runtime failure to discover a bad call
What it looks likeAssuming a mismatched call will only break when a particular input is reached.
Why it failsArgument type checking happens at compile time, at the call site.
Do this insteadRead the argument type and the parameter type at each call, and fix the mismatch when the code is written.
Common mistake Thinking parameter names must match between declaration and call
What it looks likeBelieving that the name vec in a declaration must be used at the call site.
Why it failsArgument names in a declaration are ignored by the compiler unless the declaration is also a definition. They do not form part of the function's type.
Do this insteadUse parameter names in declarations to document meaning for human readers, and remember that the caller supplies values, not names.
Practice Predicting Calls
For each call to sqrt, first say which value reaches the parameter and what type it has at that moment. Then decide whether the compiler accepts the call. Work through the call sqrt(2) and a call that passes a string literal.
Hints
- Ask what the parameter type of sqrt is.
- Ask whether an implicit conversion exists from the argument's type to that parameter type.
- If no implicit conversion exists, the compiler reports an error when the call is compiled.
A function is declared as double get(const vector<double>& vec, int index). Write its function type, leaving out the argument names. Then say whether a call that passes the int value 3 as index would compile, and explain why.
Hints
- Keep the return type and the argument types, and drop vec and index.
- Ask whether int to int needs any conversion at all.
Key Takeaways
- Argument passing follows the semantics of copy initialization: the argument's type is checked against the parameter type, and an implicit conversion is applied when it is necessary.
- A call such as sqrt(2) compiles because an implicit conversion from int to double exists. A call such as sqrt with a string literal is a compile-time error, because no implicit conversion from a string to double exists.
- Type checking happens at the call site, against the declared parameter type, so mismatches are caught before the program runs.
- A function's type is its return type together with its argument types. Argument names are ignored by the compiler unless the declaration is also a definition, and for class member functions the class name is also part of the type.