Implicit Conversions
C++ implicit conversions — standard conversion sequences, integral promotions, arithmetic conversions, narrowing, explicit constructors, and conversion operators.
Implicit Conversionsince C++98An implicit conversion is a compiler-inserted type transformation applied automatically when a value of one type appears in a context requiring another, subject to a strictly ranked sequence of allowed conversions.
Overview
The compiler constructs an implicit conversion sequence from three possible stages, applied in order:
- Zero or one standard conversion (lvalue-to-rvalue, array/function decay, integral promotion, numeric conversion, boolean conversion, qualification adjustment)
- Zero or one user-defined conversion (a converting constructor or a conversion operator)
- Zero or one standard conversion (again, for the result of stage 2)
The critical rule: at most one user-defined conversion is allowed per implicit conversion sequence. This prevents the compiler from chaining two user-defined types together without explicit programmer intent, and is the primary reason some seemingly obvious conversions are rejected.
Integral Promotions
Small integer types are widened to int (or unsigned int) before any arithmetic. This happens before the operands of +, -, *, %, bitwise operators, and similar expressions are evaluated.
char a = 200;
char b = 100;
// a and b are both promoted to int before addition
// result is int(200) + int(100) = 300, which is fine as int
int safe = a + b; // 300
char risky = a + b; // 300 truncated to char: UB if char is signed, wraps if unsigned
// Bit operations also promote
char mask = 0xFF;
char val = 0x80;
int sign_extended = val & mask; // both promoted to int; result is int, not char
// unsigned char promotes to int (not unsigned int) when int can represent all its values
unsigned char uc = 200;
auto x = uc + 1; // decltype(x) is int, value 201The "surprise" case: char arithmetic happens in int, so the result type of char + char is int, not char. Narrowing back to char on assignment is where data loss or UB occurs.
Usual Arithmetic Conversions
When binary arithmetic involves operands of different types, both are converted to a common type according to a strict hierarchy:
long double > double > floatIf neither operand is floating-point, integer promotions apply first, then:
unsigned long long > long long > unsigned long > long > unsigned int > intThe signed/unsigned interaction is the most dangerous case:
int si = -1;
unsigned int ui = 1u;
// si is converted to unsigned int: -1 wraps to UINT_MAX
bool wrong = (si < ui); // false — classic bug
// Comparison across signed/unsigned always warrants scrutiny
// C++20 introduces sign-safe comparison utilities:
#include <utility>
bool correct = std::cmp_less(si, ui); // true — C++20The long long vs unsigned int case is platform-dependent:
long long ll = -1LL;
unsigned int u = 1u;
// If sizeof(long long) > sizeof(unsigned int): u converts to long long
// Result: long long(-1) + long long(1) = 0LL (correct)
// If they were the same size: ll converts to unsigned long long (wraps — wrong)
// On all common 64-bit platforms, the first case applies
auto r = ll + u; // long long on LP64/LLP64; check with static_assert if criticalNarrowing Conversions
A narrowing conversion reduces the range or precision of a value. It is silently permitted with = and () initialization, but ill-formed with braced-list initialization (C++11):
double d = 3.99;
int i1 = d; // ok, truncates to 3 — no diagnostic required
int i2(d); // ok, same
int i3{d}; // error: narrowing conversion from double to int // C++11
int big = 300;
char c1 = big; // ok: implementation-defined if char is signed and 300 > CHAR_MAX
char c2{big}; // error: narrowing // C++11
// Exception: constant expressions that fit are not narrowing
char c3{65}; // ok: 65 fits in char (it's 'A')
char c4{300}; // error: 300 doesn't fit
// Portable range-checked narrowing (the gsl::narrow pattern):
template<typename To, typename From>
To narrow_cast(From v) {
auto result = static_cast<To>(v);
if (static_cast<From>(result) != v)
throw std::bad_cast{};
// Handle sign-change case (e.g. int(-1) → unsigned int → int(-1) passes above but is wrong)
if ((result < To{}) != (v < From{}))
throw std::bad_cast{};
return result;
}
auto c = narrow_cast<char>(65); // ok
auto x = narrow_cast<char>(300); // throwsPrefer braced initialization by default — it turns silent narrowing bugs into compile errors at no runtime cost.
User-Defined Conversions
Converting Constructors
Any constructor callable with one argument (not marked explicit) is an implicit conversion from that argument type to the class type:
struct Kg {
double value;
Kg(double kg) : value{kg} {} // implicit — intentional for natural syntax
};
struct Buffer {
explicit Buffer(std::size_t n); // explicit — "Buffer b = 5;" is wrong intent
};
struct Meter {
double value;
explicit Meter(double m) : value{m} {} // explicit — prevents weigh(5.0) silently meaning 5 meters
};
void weigh(Kg k) { /* ... */ }
weigh(5.0); // ok: Kg(5.0) constructed implicitly
weigh(Meter{5.0}); // error: Meter is not Kg, and no implicit chain allowedSince C++11, constructors taking an std::initializer_list are also user-defined conversions and participate in overload resolution.
Conversion Operators
A non-explicit conversion operator allows implicit conversion from the class:
struct Fraction {
int num, den;
operator double() const { return static_cast<double>(num) / den; } // implicit
};
Fraction f{1, 3};
double d = f; // ok
if (f) { } // also ok: f → double → bool — almost certainly unintended
// C++11: explicit conversion operators
struct SafeFraction {
int num, den;
explicit operator double() const { return static_cast<double>(num) / den; }
explicit operator bool() const { return den != 0 && num != 0; } // C++11
};
SafeFraction sf{1, 3};
double d2 = static_cast<double>(sf); // must cast explicitly
if (sf) { } // ok: if/while/for/! use "contextual conversion to bool", which accepts explicit boolThe contextual conversion to bool is a special rule (C++11): in boolean contexts (if, while, !, &&, ||, ternary condition), an explicit operator bool is used implicitly. This is why std::unique_ptr, std::optional, and streams define explicit operator bool.
Best Practices
- Mark any single-argument constructor
explicitunless implicit conversion is the deliberate design intent (e.g.,std::stringfromconst char*, or a units type with natural literal syntax). - Prefer
explicit operator bool()over implicit for all types that have a "truthy" state — contextual conversion still works inif/while. - Use braced-list initialization
{}for variable declarations to catch narrowing at compile time. - Prefer
std::cmp_less/std::cmp_equal(C++20) over raw<when comparing values of mixed signedness. - Implement
narrow_cast<To>(v)(or usegsl::narrow) for any intentional narrowing across trust boundaries.
Common Pitfalls
Signed/unsigned comparison always compares unsigned:
std::vector<int> v{1, 2, 3};
for (int i = v.size() - 1; i >= 0; --i) { } // infinite loop if v is empty:
// v.size() returns size_t (unsigned), size_t(0) - 1 wraps to SIZE_MAX
// i >= 0 is always true after conversion
// Fix: use signed index type or range-for in reverse
for (auto it = v.rbegin(); it != v.rend(); ++it) { }Multi-argument constructors with defaults create implicit conversions:
struct Timeout {
int ms;
int retries;
Timeout(int ms, int retries = 3) : ms{ms}, retries{retries} {}
// No explicit → callable with one argument → implicit conversion from int!
};
void connect(Timeout t);
connect(500); // Timeout{500, 3} — probably fine, but surprising without explicitChained user-defined conversions are silently rejected:
struct A { A(int) {} };
struct B { B(A) {} };
void f(B);
f(42); // error: requires A(int) then B(A) — two user-defined conversions, not allowed
f(A{42}); // ok: one user-defined conversion (B(A))explicit on multi-argument constructors (C++11) still matters for list-init:
struct Point {
explicit Point(int x, int y);
};
Point p1(1, 2); // ok: direct initialization
Point p2{1, 2}; // ok: direct list-initialization
Point p3 = {1, 2}; // error: copy list-initialization rejects explicit constructorsNumeric Promotions and Integral Conversions
Integral promotion widens small types to int before arithmetic, which means the result type of char + char is int, never char. The usual arithmetic conversions then determine a common type when two operands differ — floating-point types win over integer types, and within integers the ranking follows width and signedness.
// C++11
#include <cstdint>
char a = 100;
char b = 50;
auto sum = a + b; // decltype(sum) is int, not char
// Assigning back narrows — fine here (150 fits), but silent if it didn't
// unsigned char promotes to int (not unsigned int) when int can hold all values
unsigned char uc = 200;
auto r = uc + 1; // decltype(r) is int, value 201
// Usual arithmetic conversions: long double beats everything
float f = 1.0f;
double d = 2.0;
long double ld = 3.0L;
auto result = f + ld; // both convert to long double
// Classic pitfall: uint8_t arithmetic promotes to int
uint8_t x = 200;
uint8_t y = 100;
auto z = x + y; // decltype(z) is int (300), not uint8_t
// z does NOT wrap — the wrap only happens if you assign back to uint8_t
uint8_t wrapped = x + y; // 300 truncated to 44 — silent!The promotion rules interact with auto: a variable declared auto z = x + y where x and y are uint8_t will be int, not uint8_t. This is intentional — arithmetic stays in int or wider to avoid mid-expression overflow — but it surprises developers who expect auto to preserve the source types.
Pointer Conversions: null, void*, derived-to-base
Three pointer conversions are implicit: null pointer constants convert to any pointer type, any T* converts to void*, and a derived class pointer converts to a base class pointer (upcasting). None of these three require a cast.
// C++11
#include <cstddef>
// Null pointer constant
int* p1 = nullptr; // C++11 preferred
int* p2 = 0; // still valid, but nullptr conveys intent
// int* p3 = NULL; // macro — avoid; expands to 0 or (void*)0
// T* → void* implicit
int n = 42;
void* vp = &n; // ok: any T* converts to void* implicitly
// void* → T* requires explicit cast — the compiler cannot verify alignment or type
int* back = static_cast<int*>(vp); // safe: we know the dynamic type
// int* bad = vp; // error: ill-formed, requires explicit cast
// reinterpret_cast bypasses the type system entirely — avoid
// int* danger = reinterpret_cast<int*>(some_other_ptr); // UB if types are unrelated
// Derived* → Base* (upcasting) is implicit
struct Base { virtual ~Base() = default; };
struct Derived : Base { int extra; };
Derived obj;
Base* bp = &obj; // implicit upcast — safe, always valid
Derived* dp = static_cast<Derived*>(bp); // explicit downcast — programmer's responsibilityThe rule that void* → T* requires an explicit cast is a safety gate: void* carries no type information, so the compiler cannot guarantee the object on the other end is actually a T. static_cast is the correct tool when you know the dynamic type; dynamic_cast should be preferred when the type is uncertain and the base class is polymorphic.
User-Defined Conversion Functions
A conversion function (operator T() const) lets a class type implicitly convert to T; marking it explicit restricts the conversion to direct-initialization contexts and boolean contextual conversions. Conversion constructors (single-argument, non-explicit) work in the opposite direction: they allow implicit construction of the class from the argument type.
// C++11
struct Celsius {
double value;
explicit Celsius(double c) : value{c} {} // explicit constructor: Celsius c = 37.0; is an error
operator double() const { return value; } // implicit: double d = c; works
};
struct Kelvin {
double value;
explicit Kelvin(double k) : value{k} {}
explicit operator double() const { return value; } // explicit: must cast
explicit operator bool() const { return value > 0.0; } // C++11: bool context only
};
Celsius c{37.0};
double d1 = c; // ok: implicit operator double()
Kelvin k{310.15};
double d2 = static_cast<double>(k); // must be explicit
if (k) { /* ok: contextual bool conversion */ }
// bool b = k; // error: explicit operator bool not allowed here
// Conversion constructor (implicit) — allows Foo f = bar_instance;
struct Meters {
double value;
Meters(double m) : value{m} {} // implicit: Meters m = 5.0; works
};
// Single-argument constructors in containers: explicit matters
// std::vector<int> v = 5; // error if vector(size_t) were explicit — it is not,
// but: std::string s = 'x'; — works because string has implicit char* constructorThe explicit keyword on a constructor prevents it from being invoked in copy-initialization (Type obj = value;) and from participating as a user-defined conversion in implicit conversion sequences. It does not prevent direct-initialization (Type obj(value) or Type obj{value}).
Contextual Conversions: bool Context
Contextual conversion to bool is a narrower form of implicit conversion that applies specifically in boolean positions: if, while, for, !, &&, ||, and the ternary ?: condition. Unlike ordinary implicit conversion, it will invoke an explicit operator bool() — this is the special exemption that makes if (ptr) and if (sp) work even when the operator is marked explicit.
// C++11
#include <memory>
#include <optional>
// Raw pointer in boolean context
int* p = nullptr;
if (p) { /* contextual bool: p != nullptr */ }
// std::unique_ptr defines explicit operator bool()
auto sp = std::make_unique<int>(42);
if (sp) { /* ok: contextual bool conversion */ }
// int n = sp; // error: explicit operator bool is NOT available for arithmetic
// std::optional defines explicit operator bool()
std::optional<int> opt = 7;
if (opt) { /* ok */ }
bool has_value = static_cast<bool>(opt); // explicit cast also fine
// int v = opt; // error: explicit operator bool not implicitly convertible to int
// explicit operator bool participates in !, &&, ||
bool result = sp && opt; // ok: both undergo contextual bool conversion
bool neg = !sp; // ok
// NOT arithmetic — this is the key distinction from non-explicit operator bool()
struct OldStream { operator bool() const { return true; } };
struct NewStream { explicit operator bool() const { return true; } };
OldStream os;
int n1 = os + 1; // compiles: bool → int (1) → arithmetic — almost always wrong
NewStream ns;
// int n2 = ns + 1; // error: explicit operator bool not available in arithmetic contextPrefer explicit operator bool() for any type that has a "validity" or "engaged" state. The contextual conversion rule ensures if (obj) still works naturally while obj + 1 and similar abuses become compile errors.
Narrowing Conversions and Brace Initialization
Brace initialization ({}) is the compile-time firewall against narrowing: any conversion that could lose information is ill-formed in list-initialization, whereas the same conversion compiles silently with = or (). This is the main practical reason to prefer {} for variable declarations.
// C++11
double d = 3.7;
int i1 = d; // ok: silently truncates to 3
int i2(d); // ok: same truncation, no diagnostic required
// int i3{d}; // error: narrowing conversion from double to int
int big = 300;
// char c{big}; // error: 300 may not fit in signed char (range −128 to 127)
char c1 = big; // ok: implementation-defined, no diagnostic
// Exception: constant expressions that provably fit are not narrowing
char ok1{65}; // ok: 65 is 'A', fits in all char implementations
// char bad{300}; // error: 300 doesn't fit
// Banned in list-initialization:
// double → int (floating-point to integer)
// int → char (when value not a constant that fits)
// int → bool (any value → only 0/1)
// int → unsigned int (when value is negative)
// Aggregate initialization catches real bugs at compile time:
struct Point { int x; int y; };
double px = 1.5, py = 2.5;
// Point p{px, py}; // error: narrowing both members — caught at compile time
Point p2{1, 2}; // ok
// Checked narrowing for intentional conversions:
#include <stdexcept>
template<typename To, typename From>
To narrow_cast(From v) {
auto result = static_cast<To>(v);
if (static_cast<From>(result) != v) throw std::bad_cast{};
return result;
}
int safe = narrow_cast<int>(42.0); // ok
// int bad = narrow_cast<int>(3.7); // throws at runtimeUse gsl::narrow<T>() from the Guidelines Support Library for production code — it performs the same checked narrowing with a well-known name. Reserve static_cast for conversions where you have verified the value fits, and rely on {} initialization to eliminate entire classes of silent truncation bugs during development.
Conversion Sequences and Overload Resolution
Overload resolution ranks candidate functions by the quality of the implicit conversion sequence required for each argument: exact match beats promotion beats standard conversion beats user-defined conversion. When two overloads tie at the same rank, the call is ambiguous and the program is ill-formed.
// C++11
#include <string>
void foo(int);
void foo(long);
// char argument: char → int is a promotion, char → long is a standard conversion
// promotion outranks standard conversion → foo(int) is selected unambiguously
char c = 'x';
foo(c); // calls foo(int)
// Tie-breaking: two standard conversions at the same rank → ambiguous
void bar(float);
void bar(double);
// bar(1); // error: int → float and int → double are both "floating-integral conversion"
// User-defined conversions rank below standard conversions
struct MyInt {
operator int() const { return 42; }
};
void baz(int);
void baz(double);
MyInt mi;
baz(mi); // MyInt → int is user-defined; both baz(int) and baz(double) need user-defined conv
// then int vs double: exact match for baz(int) after conversion — baz(int) wins
// The one-user-defined-conversion rule: compiler will NOT chain two UDCs
struct A { A(int) {} };
struct B { B(A) {} };
void qux(B);
// qux(42); // error: int → A (UDC 1) → B (UDC 2) — two UDCs, forbidden
qux(A{42}); // ok: one UDC (A → B)
// Ambiguity from competing user-defined conversions
struct S1 { operator int() const { return 1; } };
struct S2 { operator long() const { return 2L; } };
void multi(long);
S1 s1;
multi(s1); // ok: S1 → int (UDC), int → long (standard)The ranking ensures predictable behavior for everyday code: passing a char to a function that accepts int just works, passing a double literal to a function that accepts float is a standard conversion, and two-step user-defined chains require explicit programmer intervention. When designing APIs with implicit conversions, watch for overload sets where multiple candidates require user-defined conversions — these are the most common source of "ambiguous call" errors.
See Also
- Explicit Keyword — controlling user-defined conversions
- Casts —
static_cast,reinterpret_cast, C-style casts - Aggregate Initialization — braced-init rules
- Operator Overloading — conversion operators in context