Concepts / Implicit Type Conversion in C++

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.

  • Programming

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?

  • Both compile, and the string call just returns a wrong value

  • sqrt(2) compiles; the string call is a compile-time error

  • The string call compiles; sqrt(2) is a compile-time error

  • Both fail at runtime only when executed

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

Definition

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.

Argument 2int literalImplicit conversionint to doubleParameter objectnew double,copy-initializedsqrt bodyworks with the copyvalue checkedcopied inused by function
When a call is made, where does the argument's value go, and is the parameter a new object separate from the original argument?

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.

cpp
Worked example

Following the handoff for sqrt(2)

Trace what happens when sqrt is called with the integer literal 2.

  1. 1
    Identify the argument

    The caller supplies the integer literal 2.

  2. 2
    Identify the parameter type

    sqrt declares a parameter of type double.

  3. 3
    Check the types

    The types differ: int against double.

  4. 4
    Look for an implicit conversion

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

  5. 5
    Copy into the parameter

    The converted value 2 is copy-initialized into a new double parameter object, which sqrt then uses.

Result

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.

Argument suppliedvalue at the call siteSame type asparameter?compare with declared typeImplicit conversionavailable?for example int to doubleApply conversioncompiler supplies itCopy into parametercall compilesCompile-time errorprogram is not builtcheck typesyesnoyesconverted valueno
Given an argument type and a parameter type, what branches does the compiler take to decide between exact match, implicit conversion, and a compile error?
CallArgument typeImplicit conversion to double?Result
sqrt(2)intYesCompiles; the argument is converted and copied into the parameter
sqrt("three")string literalNoCompile-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.

Best practice

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.

doublereturn type, part of thefunction typegetfunction name, not part ofthe typeconst vector<double>&argument type, part of thefunction typevecargument name, ignored bythe compilerintargument type, part of thefunction typeindexargument name, ignored bythe compiler
Which parts of a function declaration determine its type, and which parts such as parameter names are ignored when comparing function types?

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 like

    Calling sqrt with a string literal and expecting the compiler to parse the text as a number.

    Why it fails

    The compiler only applies implicit conversions that exist. A string literal has no implicit conversion to double.

    Do this instead

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

    Assuming a mismatched call will only break when a particular input is reached.

    Why it fails

    Argument type checking happens at compile time, at the call site.

    Do this instead

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

    Believing that the name vec in a declaration must be used at the call site.

    Why it fails

    Argument 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 instead

    Use parameter names in declarations to document meaning for human readers, and remember that the caller supplies values, not names.

Practice Predicting Calls

EASY

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

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.

Related concepts

More concepts starting with I