Automatic Type Conversions in C++
Understand when the compiler automatically converts between types and potential pitfalls.
What Is Implicit Type Conversion?
Implicit type conversion is the compiler producing a value of the type a context needs, out of a value of some other type, without you writing anything to ask for it. The same mechanism goes by two other names in the standard and in compiler documentation: automatic type conversion and coercion. Most of the conversions in a C++ program are of this kind.
Nothing in the program below mentions conversion, yet the compiler performs two of them:
#include <iostream>
int main()
{
int sprocketTeeth{ 16 };
double gearRatio{ 50 };
gearRatio = gearRatio / sprocketTeeth;
std::cout << "Gear ratio: " << gearRatio << '\n';
return 0;
}
Output:
Gear ratio: 3.125
gearRatio is a double, so the int literal 50 has to become the double value 50.0 before it can be stored. On the next line, gearRatio / sprocketTeeth mixes a double operand with an int operand, so sprocketTeeth is converted to double as well. That second conversion is why the answer is 3.125 and not the 3 that two int operands would have produced.
The counterpart to all this is explicit type conversion, where you name the destination type yourself with a cast such as static_cast<int>. Casts get their own lesson later in this chapter. Everything below concerns the conversions you never wrote.
The conversions discussed here turn one value into another value. Conversions that involve pointers, references, and inheritance run on the same machinery, but they need topics we have not reached, so they arrive in later chapters.
Recompute, Do Not Reinterpret
Ask why a conversion is needed at all and the answer sits at the hardware level: a stored object is only a run of bits, and those bits say nothing about what they mean. The type is the compiler's record of how to read them, and two types can record the same number in layouts that have nothing in common.
| Type | How its bits are organised | Holding the value 50 |
|---|---|---|
int (32 bits on a typical desktop) |
the whole width is one two's complement number, top bit negative | the low bits count straight up to 50 |
double (64 bits) |
one sign bit, 11 exponent bits, 52 fraction bits | exponent and fraction combine to give 1.5625 x 2^5 |
No field of the double lines up with any field of the int. If the compiler lifted the 32 bits that spell int 50 and dropped them into a double, the receiving object would read those bits as a sign, an exponent, and a fraction, and report some arbitrary number nowhere near 50.
A conversion is therefore not a copy. It is a small calculation: take the source value, work out which bit pattern the destination type uses for that value, and write that pattern instead. Recompute, do not reinterpret.
Because it is a calculation, it needs somewhere to put its answer, and that somewhere is a fresh object. The value being converted is only an input, and it comes out the other side untouched:
#include <iostream>
double metresPerPedalStroke(double teeth)
{
return teeth / 16 * 2.096;
}
int main()
{
int chainringTeeth{ 53 };
std::cout << "Distance per stroke: " << metresPerPedalStroke(chainringTeeth) << " m" << '\n';
std::cout << "chainringTeeth still holds the int " << chainringTeeth << '\n';
std::cout << "and int arithmetic still applies: " << chainringTeeth / 16 << '\n';
return 0;
}
Output:
Distance per stroke: 6.943 m
chainringTeeth still holds the int 53
and int arithmetic still applies: 3
metresPerPedalStroke wants a double, so the call converts chainringTeeth and hands the function a temporary double holding 53.0. Back in main, nothing has happened to chainringTeeth. It is still an int, which is why chainringTeeth / 16 still truncates to 3 on the last line.
Converting a value to another type produces a temporary object of the target type holding the result. The object you converted from is never modified, and it never changes type.
Five Places The Compiler Converts For You
Implicit conversions are not scattered at random through the language. They are triggered by a small set of contexts, each one a place where the type you supplied and the type the language demands can differ.
| Context | Example | Conversion applied |
|---|---|---|
| Initializing or assigning a variable | double gearRatio{ 50 }; then gearRatio = 52; |
int to double |
| Returning a value | return 2.096; from a function declared float |
double to float |
| A binary operator with mixed operands | gearRatio / sprocketTeeth |
int to double |
| Using a non-Boolean value as a condition | if (sprocketTeeth) |
int to bool |
| Passing an argument to a parameter | reportSprocket(sprocketTeeth) |
int to long |
Here are all five inside one program:
#include <iostream>
float wheelCircumference()
{
return 2.096;
}
void reportSprocket(long teeth)
{
std::cout << "Sprocket: " << teeth << " teeth" << '\n';
}
int main()
{
double gearRatio{ 50 };
gearRatio = 52;
int sprocketTeeth{ 16 };
gearRatio = gearRatio / sprocketTeeth;
if (sprocketTeeth)
{
reportSprocket(sprocketTeeth);
}
std::cout << "Gear ratio: " << gearRatio << '\n';
std::cout << "Rollout: " << gearRatio * wheelCircumference() << " m" << '\n';
return 0;
}
Output:
Sprocket: 16 teeth
Gear ratio: 3.25
Rollout: 6.812 m
Every context in the table fires in that program, and not one of the conversions is visible as syntax. wheelCircumference returns the double literal 2.096 from a function declared to return float, so the value is converted on the way out. The if treats 16 as true because any non-zero value converts to bool true. And reportSprocket takes a long, so the int argument is widened before the function body ever runs.
Where The Rules Come From
The compiler is not improvising any of this. The C++ standard defines a fixed catalogue of conversion rules known as the standard conversions. They specify how the fundamental types, plus certain compound types such as arrays, references, pointers, and enumerations, convert to other types within that same group.
As of C++23 the catalogue holds 14 conversions, sorted into 5 categories:
| Category | Conversions in it | What it covers |
|---|---|---|
| Value transformations | 4 | Changing an expression's value category, plus array-to-pointer and function-to-pointer decay |
| Numeric promotions | 2 | Small integral types widening to int or unsigned int, and float widening to double |
| Numeric conversions | 4 | Every other integral and floating point conversion, including integral to floating point and anything to bool |
| Pointer conversions | 3 | nullptr to a pointer type, a pointer to void* or to a base class, and pointer-to-member conversions |
| Qualification conversions | 1 | Adding or removing const or volatile |
Turning an int into a float, then, is not an open question. That pairing lands in the numeric conversions category, and the compiler applies the numeric conversion rules for it.
Two of the five categories carry nearly all of the everyday traffic, and they are the next two lessons: numeric promotions, then numeric conversions. The other three become relevant once you are working with const, with arrays, and with pointers, so they are covered alongside those topics.
Three Ways A Conversion Can Fail
Whenever a conversion is required, the compiler first works out whether it can get from the source type to the destination type at all. If it can, it produces the new value. If it cannot, compilation stops with an error. Three distinct situations lead to that refusal.
1. No rule connects the two types
The program below does not compile:
int main()
{
int sprocketTeeth{ "16" };
return 0;
}
GCC reports:
s.cpp: In function 'int main()':
s.cpp:3:24: error: invalid conversion from 'const char*' to 'int' [-fpermissive]
3 | int sprocketTeeth{ "16" };
| ^~~~
| |
| const char*
A string literal has type const char*, and the catalogue contains no standard conversion from const char* to int. The search ends with nothing to apply.
2. A rule exists, but the context forbids it
This one also fails to compile, for a completely different reason:
int main()
{
int sprocketTeeth{ 16.5 };
return 0;
}
GCC reports:
s.cpp: In function 'int main()':
s.cpp:3:24: error: narrowing conversion of '1.65e+1' from 'double' to 'int' [-Wnarrowing]
3 | int sprocketTeeth{ 16.5 };
| ^~~~
The compiler knows exactly how to turn a double into an int. What it will not do is apply that conversion inside braces, because discarding the .5 loses part of the value. Conversions that can lose data this way are called narrowing conversions, and brace initialization rejects them outright.
That rejection is the whole point of the braces. The next program is what you should not write: the same conversion under copy initialization, which has no such rule and lets the loss through:
#include <iostream>
int main()
{
int sprocketTeeth = 16.5;
std::cout << "Stored sprocket teeth: " << sprocketTeeth << '\n';
return 0;
}
Output:
Stored sprocket teeth: 16
That program compiles cleanly with warnings enabled, and half a tooth vanishes without comment. The conversion was legal, so nothing objected. Brace initialization would have stopped it at compile time.
If truncation really is what you want, request it with a cast so the loss is visible to the next reader:
#include <iostream>
int main()
{
double measuredTeeth{ 16.5 };
int sprocketTeeth{ static_cast<int>(measuredTeeth) };
std::cout << "Truncated to " << sprocketTeeth << " teeth" << '\n';
return 0;
}
Output:
Truncated to 16 teeth
3. Several rules apply and none of them wins
The third failure mode needs more than one candidate. When a name refers to several overloaded functions, the compiler has to score the conversions each candidate would require and pick a winner. Sometimes two candidates tie, and an ambiguous call is an error even though each individual conversion is perfectly valid on its own. That machinery is function overload resolution, and it has its own lesson later in the course.
static_cast.
Summary
Implicit type conversion is applied by the compiler on its own, with no cast in the source, wherever an expression of one type appears in a context that requires a different type. Its other names are automatic type conversion and coercion.
Conversions recompute, they do not reinterpret. Two types can store the same number in unrelated bit layouts, so the compiler cannot copy bits across. It reads the source value and builds the destination type's representation of that value from scratch.
The original is never touched. A conversion produces a temporary object of the target type holding the result. The object converted from keeps its value and its type.
Five contexts trigger implicit conversions: initializing or assigning a variable, returning a value, applying a binary operator to mixed operand types, using a non-Boolean value as a condition, and passing an argument whose type differs from the parameter.
The rules are a fixed catalogue. As of C++23 the standard defines 14 standard conversions grouped into 5 categories: value transformations, numeric promotions, numeric conversions, pointer conversions, and qualification conversions.
Conversions fail in three ways: no rule connects the two types, a rule exists but the context forbids it (brace initialization rejecting a narrowing conversion), or several candidate conversions tie and the call is ambiguous. All three are compile errors, not runtime surprises.
The next two lessons open up the two categories you will meet most often, starting with the conversions that are always safe.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Automatic Type Conversions in C++ - Quiz
Test your understanding of the lesson.
Practice Exercises
Understanding Implicit Type Conversion
Explore how C++ automatically converts between types. Learn about numeric promotions, conversions, and potential pitfalls.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!