Skip to content
C++
Language
since C++98
Intermediate

Converting Constructor

A constructor not declared explicit, enabling implicit conversions from its argument types to the class type during initialization and function calls.

Converting Constructorsince C++98

A constructor not declared explicit that the compiler may invoke automatically to convert one or more argument values into an object of the enclosing class type.

Overview

In C++98 and C++03, converting constructor referred specifically to a constructor taking exactly one argument (excluding the copy constructor). Such a constructor defined a user-defined implicit conversion from the argument type to the class type.

Since C++11, the definition broadened: any constructor not declared explicit is a converting constructor, regardless of the number of parameters. This includes zero-argument constructors, multi-argument constructors, and constructors accepting std::initializer_list. The broadening matters because braced-init-list initialization allows multi-argument constructors to participate in implicit conversion sequences.

The compiler invokes converting constructors in three main contexts:

  • Copy initialization β€” T x = expr;, passing an argument to a by-value parameter, returning a value
  • Explicit conversion syntax β€” static_cast<T>(expr), T(expr)
  • Contextual conversions β€” boolean conditions, switch operands, and a handful of other contexts

Copy initialization is the most restrictive context: it will not use an explicit constructor. Direct initialization (T x(expr)) will use explicit constructors. This asymmetry is the core reason explicit exists.

Syntax

cpp
class Celsius {
public:
    Celsius(double degrees) : degrees_(degrees) {}       // converting constructor
    explicit Celsius(int whole) : degrees_(whole) {}     // NOT a converting constructor
    Celsius() : degrees_(0.0) {}                         // converting constructor (C++11: any non-explicit)
private:
    double degrees_;
};

explicit may appear only on the declaration inside the class body; repeating it on an out-of-class definition is ill-formed.

Since C++20, explicit accepts a constant boolean expression, enabling conditional explicitness in templates:

cpp
template<typename T>
class Wrapper {
public:
    // C++20: explicit only when T is not implicitly convertible from int
    explicit(!std::is_convertible_v<int, T>) Wrapper(T val) : val_(val) {}
private:
    T val_;
};

Examples

Implicit conversion in function calls

cpp
#include <string>
#include <string_view>

class LogMessage {
public:
    LogMessage(std::string_view text, int severity = 0)
        : text_(text), severity_(severity) {}
private:
    std::string text_;
    int severity_;
};

void emit(LogMessage msg);

void example() {
    emit("disk full");           // OK: const char* -> string_view -> LogMessage
    emit({"disk full", 3});      // C++11: braced-init triggers multi-arg converting ctor
    LogMessage m = "startup";    // OK: copy initialization via converting constructor
}

Suppressing unwanted conversions

The classic pitfall is a container-like class whose capacity constructor doubles as an unintended converting constructor:

cpp
#include <vector>
#include <cstring>

class Buffer {
public:
    explicit Buffer(std::size_t capacity) : data_(capacity) {}

    Buffer(const char* literal)           // intentionally non-explicit
        : data_(literal, literal + std::strlen(literal)) {}
private:
    std::vector<char> data_;
};

void process(Buffer b);

void example() {
    process(4096);          // Error: explicit constructor not used in copy init
    process(Buffer{4096});  // OK: direct initialization
    process("hello");       // OK: non-explicit const char* constructor
}

Without explicit on the size constructor, process(4096) silently allocates a 4096-byte buffer β€” a bug that compiles without warning.

Overload resolution and ambiguity

Converting constructors widen the implicit conversion candidates available to the overload resolver. When multiple overloads accept different types and both those types have non-explicit constructors from the same source type, the call becomes ambiguous:

cpp
struct Meters  { Meters(double d)  : v(d) {} double v; };
struct Seconds { Seconds(double d) : v(d) {} double v; };

void move(Meters distance);
void move(Seconds duration);

void example() {
    move(10.0);  // Error: ambiguous β€” 10.0 converts to both Meters and Seconds
}

Marking the constructors explicit eliminates the ambiguity and forces call sites to name the type:

cpp
struct Meters  { explicit Meters(double d)  : v(d) {} double v; };
struct Seconds { explicit Seconds(double d) : v(d) {} double v; };

move(Meters{10.0});   // OK
move(Seconds{10.0});  // OK

std::initializer_list constructors take priority (C++11)

When a class has a constructor taking std::initializer_list<T>, it wins over other converting constructors whenever braced-init-list syntax is used β€” even if element-wise construction would match better:

cpp
#include <initializer_list>

class Oddity {
public:
    Oddity(int n)                         {}   // (1)
    Oddity(std::initializer_list<int> il) {}   // (2) β€” C++11
};

Oddity a(42);    // calls (1)
Oddity b{42};    // calls (2) β€” initializer_list wins
Oddity c = 42;   // calls (1) β€” copy init, no braces
Oddity d = {42}; // calls (2) β€” braces trigger list constructor

Adding an initializer_list overload to an existing class can silently reroute all braced constructions. Audit every call site when doing so.

emplace functions bypass explicit (C++11)

std::vector::emplace_back uses direct initialization internally, so an explicit constructor remains reachable through emplace_back(args...) even though push_back would reject the same arguments:

cpp
#include <vector>
#include <regex>

std::vector<std::regex> patterns;

patterns.push_back("\\d+");          // Error: std::regex(const char*) is explicit
patterns.emplace_back("\\d+");       // OK: direct initialization bypasses explicit

This is intentional behavior β€” emplace is the caller explicitly opting into direct construction β€” but it can surprise readers who assume explicit provides a total barrier.

Best Practices

Default to explicit for single-argument constructors. The C++ Core Guidelines (C.46) codify this. A non-explicit single-argument constructor creates an implicit user-defined conversion that participates in overload resolution invisibly to callers. The resulting failures β€” wrong overload silently selected, unexpected temporaries allocated, narrowing conversions accepted without warning β€” are disproportionately hard to diagnose.

Allow implicit conversions only when they are part of the documented contract. std::string(const char*) is non-explicit because every C++ programmer expects that conversion. std::complex<double>(double) is non-explicit because the mathematical identity complex = real + 0i makes the conversion semantically sound. Apply this standard before leaving a constructor non-explicit.

Use explicit(condition) in generic wrappers (C++20). Wrapper types should mirror the explicitness of what they wrap:

cpp
template<typename T>
struct Box {
    template<typename U>
    explicit(!std::is_convertible_v<U, T>) Box(U&& val) : val_(std::forward<U>(val)) {}
    T val_;
};

Box<int> a = 42;    // OK: int -> int is implicit, so Box<int>(42) is non-explicit
Box<std::regex> b = "\\d+"; // Error: regex(const char*) is explicit, so Box<regex> mirrors it

Mark all constructors explicit in strong-typedef and unit-quantity types. When building distinct types for dimensioned quantities (Meters, Seconds, Kilograms), unconditional explicit prevents accidental cross-unit conversion at the cost of slightly more verbose construction β€” a good trade.

Common Pitfalls

Adding explicit retroactively breaks distant call sites non-locally. The compiler error appears at the call site (f(value)), not at the constructor. In large codebases the relationship between the error and the change is opaque. Treat the explicitness of a public constructor as part of its API contract.

At most one user-defined conversion is applied per implicit conversion sequence. If converting A β†’ C requires A β†’ B then B β†’ C, and both steps are user-defined, the compiler will not chain them. This limit contains conversion surprises but does not eliminate them β€” a single-step conversion can still fire in unexpected contexts.

Braced-init and parenthesized init select different overloads. Mechanically substituting () with {} anywhere in a codebase is not a safe refactoring if any class involved has an initializer_list constructor. Treat them as distinct initialization syntaxes with distinct overload resolution rules.

Multi-Parameter Converting Constructors (C++11)

Before C++11, "converting constructor" meant a non-explicit constructor taking exactly one argument. The term was meaningful because only single-argument constructors could appear as the user-defined step inside an implicit conversion sequence applied to a single expression.

C++11 changed two things simultaneously:

  1. Definition broadened. Any constructor not declared explicit is now a converting constructor, including zero-argument, two-argument, and std::initializer_list constructors.
  2. explicit extended to multi-parameter constructors. Marking a multi-parameter constructor explicit is now meaningful because braced-init-list syntax (T x = { a, b }) creates a context β€” copy-list-initialization β€” where an explicit constructor is disallowed.

A constructor taking several parameters with defaults is still a converting constructor for every arity at which it can be called with a single argument:

cpp
class Connection {
public:
    // Two-argument constructor, second has a default.
    // Called as Connection("host") β€” still a converting constructor in C++11.
    Connection(std::string host, int port = 5432)
        : host_(std::move(host)), port_(port) {}
private:
    std::string host_;
    int port_;
};

void open(Connection c);

void example() {
    open("localhost");         // OK: "localhost" -> std::string -> Connection
    open({"localhost", 5433}); // OK: braced list triggers multi-arg converting ctor
    // open({"localhost", 5433, "extra"}); // Error: no matching constructor
}

When you want to prevent both the single-argument and the braced-list forms, explicit must be applied to the constructor, not only to specific overloads:

cpp
class Connection {
public:
    explicit Connection(std::string host, int port = 5432)
        : host_(std::move(host)), port_(port) {}
private:
    std::string host_;
    int port_;
};

void open(Connection c);

void example() {
    open("localhost");             // Error: explicit constructor in copy-init context
    open(Connection{"localhost"}); // OK: direct initialization
    open({"localhost", 5433});     // Error: explicit constructor in copy-list-init
}

The rule is symmetric: explicit on a multi-parameter constructor suppresses the braced-list copy-initialization path (T x = { a, b }), while direct-list-initialization (T x{ a, b }) still invokes it. This gives library authors fine-grained control over which construction syntaxes are available to callers.

Copy-Initialization vs. Direct-Initialization

The distinction between copy-initialization and direct-initialization is the precise mechanism by which explicit does its work. Understanding both forms β€” and their braced variants β€” is essential for predicting which constructors fire in any given context.

The four initialization syntaxes

cpp
struct Widget {
    explicit Widget(int n);     // (E) explicit
    Widget(double d);           // (I) implicit / converting constructor
};

Widget a(42);      // direct-init:           (E) and (I) are both candidates
Widget b = 42;     // copy-init:             only (I) is a candidate (42 -> double -> Widget)
Widget c{42};      // direct-list-init:      (E) and (I) are candidates; narrowing int->double blocked
Widget d = {42};   // copy-list-init:        only (I) is a candidate; narrowing blocked

The table below summarizes the rules:

SyntaxNameexplicit ctor allowed?Narrowing allowed?
T x(expr)direct-initializationyesyes
T x = exprcopy-initializationnoyes (standard convs)
T x{expr}direct-list-initializationyesno
T x = {expr}copy-list-initializationnono

Where copy-initialization silently appears

Copy-initialization is not limited to T x = expr. It also arises in contexts that look nothing like assignments:

  • Passing a by-value argument: void f(Widget w); f(42); β€” copy-initializes the parameter
  • Returning by value: Widget g() { return 42; } β€” copy-initializes the return value
  • Throwing an exception: throw 42; β€” copy-initializes the exception object
  • Aggregate member initialization with =: struct S { Widget w = 42; };

In every one of those contexts, explicit constructors are excluded from the candidate set. The return 42; form is a common surprise: it looks like a direct construction inside the function body but uses copy-initialization semantics.

The T x = {expr} subtlety

T x = {val} is copy-list-initialization: the braces do not make it direct. The outer = still forces copy-init semantics, so explicit constructors are excluded. This trips up developers who think braces always mean "I'm explicitly constructing this":

cpp
struct Token {
    explicit Token(int id);
};

Token t1{99};       // OK: direct-list-init
Token t2 = {99};    // Error: copy-list-init, explicit ctor excluded

Prefer T x{val} over T x = {val} when you want unambiguous direct-list-initialization.

explicit(bool) β€” Conditional Explicitness (C++20)

C++20 (P0892) extended explicit to accept a compile-time constant boolean expression. When the expression is true, the constructor is explicit; when it is false, the constructor is a converting constructor. The feature exists specifically for generic wrappers that want to mirror the implicitness of whatever they hold.

Syntax

cpp
template<typename T>
class Optional {
public:
    template<typename U = T>
    explicit(!std::is_convertible_v<U, T>) Optional(U&& val);
};

!std::is_convertible_v<U, T> evaluates to true when U is not implicitly convertible to T β€” meaning the Optional constructor becomes explicit exactly when the underlying conversion is explicit. This preserves the semantics of the wrapped type at the wrapper level.

Standard library usage

std::pair, std::tuple, std::optional, and std::expected all use this pattern in their converting constructors since C++20. Before C++20, these types had to provide two overloads β€” one explicit, one not β€” distinguished by SFINAE constraints. The explicit(bool) form collapses both overloads into one:

cpp
// Pre-C++20 (two overloads required):
template<typename U1, typename U2,
         std::enable_if_t<is_implicit_pair<U1, U2>, int> = 0>
pair(U1&&, U2&&);                        // non-explicit overload

template<typename U1, typename U2,
         std::enable_if_t<!is_implicit_pair<U1, U2>, int> = 0>
explicit pair(U1&&, U2&&);               // explicit overload

// C++20 (one overload):
template<typename U1, typename U2>
explicit(!std::is_convertible_v<U1, T1> || !std::is_convertible_v<U2, T2>)
pair(U1&&, U2&&);

A practical wrapper example

cpp
#include <type_traits>

template<typename T>
struct Box {
    template<typename U,
             typename = std::enable_if_t<std::is_constructible_v<T, U>>>
    explicit(!std::is_convertible_v<U, T>) Box(U&& val)
        : val_(std::forward<U>(val)) {}

    T val_;
};

// std::string is implicitly constructible from const char*, so Box<string> mirrors that:
Box<std::string> a = "hello";     // OK: non-explicit (mirrors std::string(const char*))

// std::regex is explicitly constructible from const char*, so Box<regex> mirrors that:
Box<std::regex> b = "\\d+";       // Error: explicit (mirrors std::regex(const char*))
Box<std::regex> c{"\\d+"};        // OK: direct-list-init can invoke explicit ctor

The net effect is that wrapping a type in Box does not change the implicitness of its construction β€” callers get exactly the same conversion behavior at the wrapper level as they would at the inner type level.

User-Defined Conversion Sequences

The C++ standard (over-loaded resolution rules, [over.best.ics]) limits an implicit conversion sequence to at most one user-defined conversion. A user-defined conversion is either a converting constructor or a conversion function (operator T()). This cap prevents the compiler from chaining multiple class-type conversions automatically.

The one-UDC rule in practice

cpp
struct A { A(int); };          // converting constructor: int -> A
struct B { B(A); };            // converting constructor: A -> B
struct C { C(B); };            // converting constructor: B -> C

void take_c(C);

take_c(1);   // Error: would need int->A->B->C β€” two user-defined conversions
take_c(A{1}); // Error: still A->B->C β€” still two user-defined conversions
take_c(B{A{1}}); // OK: B->C is a single user-defined conversion
take_c(C{B{A{1}}}); // OK: explicit construction, no implicit conversion at all

The rule is enforced per implicit conversion sequence, not per call expression. Each implicit sequence may contain:

  • Zero or one standard conversion (e.g., int β†’ long, pointer decay)
  • Zero or one user-defined conversion (constructor or operator T())
  • Zero or one standard conversion after the user-defined step

A call needing two user-defined conversions is simply not a viable implicit sequence β€” the compiler will report no matching function rather than chaining the conversions.

Why this matters for API design

If A β†’ B β†’ C is a semantically meaningful chain, you must either:

  1. Provide C(A) directly (explicit bridge constructor), or
  2. Require callers to write C{B{a}} to make each step visible.

The one-UDC rule is a deliberate language safety mechanism. Without it, adding a new converting constructor anywhere in a type hierarchy could silently activate multi-step chains across unrelated types.

Conversion Operator vs. Converting Constructor in Overload Resolution

When both a conversion function on the source type and a converting constructor on the target type could satisfy an implicit conversion, overload resolution must pick one. The rules are more nuanced than "constructor wins":

Both exist β€” the ambiguity trap

cpp
struct Celsius {
    operator double() const { return degrees_; }   // conversion function
    double degrees_;
};

struct Fahrenheit {
    Fahrenheit(double c) : degrees_(c * 9.0 / 5.0 + 32.0) {}  // converting constructor
    double degrees_;
};

void print(Fahrenheit f);
Celsius c{100.0};

print(c);   // Ambiguous? No β€” Celsius::operator double() + Fahrenheit(double) is ONE chain:
            //   Celsius -> double (user-defined) -> Fahrenheit(double) (user-defined)
            //   Two user-defined conversions: not viable.

In this case, both conversions together exceed the one-UDC limit, so the call fails β€” not because of ambiguity but because no single-step implicit path exists. The caller must write print(Fahrenheit{c.degrees_}) or print(static_cast<Fahrenheit>(static_cast<double>(c))).

When both a ctor and an operator exist for the same path

Ambiguity arises when a single conversion step can be accomplished by either a conversion function on the source or a converting constructor on the target:

cpp
struct Meters {
    operator double() const { return val_; }
    double val_;
};

struct Distance {
    Distance(double d) : val_(d) {}   // converting constructor from double
    double val_;
};

// Meters -> double (via operator double) -> Distance(double)   β€” two UDCs, NOT viable
// But if Distance(Meters) also existed:
struct Distance2 {
    Distance2(Meters m) : val_(m.val_) {}   // converting constructor from Meters directly
    Distance2(double d) : val_(d) {}
    double val_;
};

void measure(Distance2 d);
Meters m{5.0};
measure(m);   // Ambiguous: Distance2(Meters) vs. Meters::operator double() + Distance2(double)
              //            Both are single-step implicit sequences.

This is a genuine overload-resolution ambiguity: both Distance2(Meters) and the chain Meters::operator double() β†’ Distance2(double) are single-UDC sequences of equal rank. The compiler reports an ambiguity error and requires the caller to cast.

Practical guidance: When a source type already has operator T(), avoid also adding Target(SourceType) for the same conversion pair. Pick one side. Prefer the converting constructor on the target when the target type is yours, because it keeps the source type unaware of the target.

C++ Core Guidelines

The Core Guidelines encode several decades of collective experience with converting constructors. The most relevant rules:

C.46 β€” By default, declare single-argument constructors explicit. The guideline reflects that implicit single-argument conversions are almost always surprising to callers. Non-explicit single-argument constructors should be the exception, justified by a clear semantic identity between the source and target types.

ES.23 β€” Prefer the {} initializer syntax. Braced initialization catches narrowing conversions at compile time (e.g., int β†’ float with loss of precision) that = initialization would silently allow. When combined with explicit constructors, it also forces the programmer to state the intended type explicitly via direct-list-initialization:

cpp
double precision = 3.14159;

// These silently truncate:
int bad1 = precision;       // OK (warning with -Wconversion), value is 3
int bad2(precision);        // OK (same), no narrowing diagnostic required

// These are ill-formed (narrowing):
int good1{precision};       // Error: narrowing conversion from double to int
int good2 = {precision};    // Error: narrowing conversion from double to int

For class types with converting constructors, ES.23 reduces surprise because T x{expr} makes the conversion visible at the call site, and the narrowing check prevents silent integer or floating-point truncation from sneaking through the constructor's parameter.

C.164 β€” Avoid implicit conversion operators. The guideline prefers explicit operator T() over non-explicit conversion functions for the same reason C.46 favors explicit constructors: invisible conversion operators participate in overload resolution and can fire in contextual-conversion contexts (if (obj)) with results the author did not anticipate.

Together these three rules push toward a coding style where implicit conversions are opt-in, narrowly scoped, and semantically justified β€” rather than the default outcome of forgetting to write explicit.

See Also

  • explicit specifier
  • std::initializer_list (C++11)
  • Copy initialization vs. direct initialization
  • reference/language/aggregate-initialization
  • reference/language/class-template