C++ converts between types automatically more often than most programmers realise. Implicit type conversion works behind the scenes, making our life easier. However, just as it helps it also bites. This Nibble will explore this double-edged sword.
Where implicit conversion is useful
Implicit type conversion is frequently used, more times than we realize. In each case below, we intentionally let the compiler perform implicit type conversion. If explicit conversion via static_cast was required, it would’ve added noise and clutter.
// 1. Variable initialisation with a literal of a different type.
double price = 3; // int -> double
// 2. Return statement converting to the declared return type.
string_view printMonster() { return "Skeleton" } // C-style string lit -> string_view
// 3. Mixed arithmetic, where operands of different types meet.
double result = 3.0 / 4; // 4 promoted to double, result is 0.75
// 4. Function calls where argument and parameter types differ.
void log(long n);
log(42); // int -> long
// 5. Conditions, where any pointer or numeric value can be tested.
int* p = find_node();
if (p) { /* p is non-null */ } // int* -> bool
// 6. An int parameter accepting a double argument
int calculateDiscount(int price)
int main() {
double shoes { 99.69};
price = calculateDiscount
}
// 7. Initializing a float with an int
float desktopTable { 100 };
These conversions are why C++ feels reasonably ergonomic and natural to write. Forcing every literal 0 to be written 0.0 when initialising a double, or every pointer test to be written if (p != nullptr), would be tedious and would not make code clearer.
Where implicit conversion bites
Truncation via numeric conversion
int x = 3.7; // x is 3 — the .7 is silently dropped
Overflow when a value doesn’t fit in the target type:
short s = 100000; // 100000 doesn't fit in 16-bit short, behaviour
// is implementation-defined (often wraps to a
// negative number)
Sign change when mixing signed and unsigned:
unsigned int u = -1; // u is 4'294'967'295, not -1
Any non-zero value becoming true:
bool valid = 42; // valid is true; the 42 is gone
In every case the compiler accepts the code without complaint. This is a source of bugs.
NOTE: Brace initialization guards against narrowing conversions.
*int c { 3.7 }; // COMPILE ERROR: narrowing conversion not allowe*d
How the compiler performs implicit conversion
The compiler doesn’t magically do this (even though it appears so). Rather, the compiler performs these conversion via a rulebook called standard conversions.
It’s used by the compiler to perform type conversions.
As of C++23, there are 14 different standard conversions. These can be grouped into five categories.
| Category | Example |
|---|---|
| Numeric promotions | short → int, float → double |
| Numeric conversions | int ↔ double, long → int, int → bool |
| Qualification | T* → const T* |
| Value transformations | lvalue → rvalue |
| Pointer conversions | nullptr → T*, Derived* → Base* |
Promotions are always safe — they widen a value into a type that can hold every possible source value.
Numeric conversions are the ones that can lose information; this is the row that contains truncation, overflow, and sign issue
Please see Clause 7.3 Standard Conversions of the ISO/IEC 14882 standard for the extended explanation.
In Summary
Implicit type conversion is one of the most pervasive and most useful features of C++. It’s used more times than we realize it.