#057 – Implicit type conversion: where C++ helps, and where it bites

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.

CategoryExample
Numeric promotionsshort → intfloat → double
Numeric conversionsint ↔ doublelong → intint → bool
QualificationT* → const T*
Value transformationslvalue → rvalue
Pointer conversionsnullptr → 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.