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.

See Also

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