C++ explicit: Blocking Implicit Conversions, explicit operator bool, and C++20 explicit(bool)

Key takeaways

A constructor that takes one argument doubles as an implicit conversion, so a function expecting a class type may silently accept an int. The post shows the bugs this causes, how explicit interacts with copy initialization and function arguments, the special rule that lets explicit bool work in conditions, and a simple rule for when to mark constructors explicit.

What is explicit?

The explicit keyword is used with constructors and conversion operators to prevent implicit conversions. It is particularly useful for avoiding unintended conversions during copy initialization with = expr. Most constructors for smart pointers are also explicit for this reason.

Comparison: With and Without explicit

CaseWithout explicitWith explicit
Copy InitializationString s = 10; ✅String s = 10; ❌
Direct InitializationString s(10); ✅String s(10); ✅
Brace InitializationString s{10}; ✅String s{10}; ✅
Function Argumentfunc(10); ✅func(10); ❌
Return Valuereturn 10; ✅return 10; ❌
static_caststatic_cast<String>(10) ✅static_cast<String>(10) ✅
class String {
public:
    String(int size) {}  // Allows implicit conversion
};

String s = 10;  // OK: int -> String

// Using explicit
class String {
public:
    explicit String(int size) {}
};

// String s = 10;  // Error
String s(10);  // OK: Explicit initialization

The reason the language needs this keyword at all is that C++ treats every constructor callable with one argument as a converting constructor. When overload resolution or initialization needs a String and finds an int, the compiler is allowed to insert exactly one user-defined conversion into the chain — and a converting constructor counts as one. explicit removes the constructor from that candidate set for implicit conversions while leaving it available whenever you name the type yourself: String(10), String{10}, or static_cast<String>(10).

One row of the table deserves a caveat: brace initialization comes in two forms. String s{10}; is direct-list-initialization and works with an explicit constructor, but String s = {10}; is copy-list-initialization and is rejected. The same applies to passing {10} as a function argument or writing return {10};. The error message from GCC is typically converting to 'String' from initializer list would use explicit constructor 'String::String(int)', which is a useful string to recognize because it looks unrelated to the line you just wrote.

Conversion Process Diagram

graph LR
    A[int value] -->|Without explicit| B[Implicit Conversion]
    B --> C[Constructor Call]
    C --> D[Object Creation]
    
    A -->|With explicit| E{Initialization Method}
    E -->|Copy Initialization =| F[Compile Error]
    E -->|Direct Initialization| G[Constructor Call]
    G --> D

Using explicit with Constructors

Problem Scenario

graph TD
    A[Function Call: process 10] --> B{Constructor explicit?}
    B -->|No| C[Allows Implicit Conversion]
    C --> D[Temporary Array 10 Object Created]
    D --> E[Function Execution]
    E --> F[Unintended Behavior]
    
    B -->|Yes| G[Compile Error]
    G --> H[Explicit Conversion Required]
    H --> I[process Array 10]
    I --> J[Clear Intent]
class Array {
public:
    // ❌ Allows implicit conversion
    Array(int size) {}
};

void process(Array arr) {}

int main() {
    process(10);  // int -> Array (unintended)
}

// ✅ Using explicit
class Array {
public:
    explicit Array(int size) {}
};

// process(10);  // Error
process(Array(10));  // OK

The damage in the non-explicit version is not just stylistic. process(10) compiles, allocates a ten-element temporary Array, passes it by value, and destroys it — all without a single visible type name at the call site. The typical way this bites is overloads: if someone later adds void process(int count), calls silently switch to the new overload because an exact match beats a user-defined conversion. With explicit, every call that wanted an Array already says so, and adding an overload cannot change what existing code means.

The first time I tracked down one of these bugs, the symptom was a function that “ignored” its argument: a Buffer type had a non-explicit Buffer(std::size_t capacity) constructor, and a caller passed a byte count where a buffer was expected. The code compiled cleanly, ran, and operated on an empty buffer of the right capacity. Nothing in a code review looks wrong in that call, which is exactly why the fix belongs in the class definition rather than in the discipline of every caller.

Practical Examples

Example 1: Basic Usage

class Vector {
    double* data;
    size_t size;
    
public:
    explicit Vector(size_t s) : size(s) {
        data = new double[size];
    }
    
    ~Vector() {
        delete[] data;
    }
};

void process(Vector v) {}

int main() {
    // process(10);  // Error
    process(Vector(10));  // OK
}

Example 2: Conversion Operator

class Fraction {
    int numerator, denominator;
    
public:
    Fraction(int n, int d) : numerator(n), denominator(d) {}
    
    // ❌ Implicit Conversion
    operator double() const {
        return (double)numerator / denominator;
    }
};

Fraction f(1, 2);
double d = f;  // Implicit conversion

// ✅ Explicit Conversion Operator
class Fraction {
public:
    explicit operator double() const {
        return (double)numerator / denominator;
    }
};

// double d = f;  // Error
double d = static_cast<double>(f);  // OK

An implicit operator double() is more dangerous than it looks because it participates in arithmetic. With it in place, f + 1, f == 0.5, and even std::sqrt(f) all compile by converting the fraction to a floating-point value, silently discarding exactness. If you later add operator+(Fraction, Fraction), some expressions become ambiguous and others quietly keep using the lossy double path. Marking the operator explicit keeps Fraction arithmetic exact by default and makes every lossy conversion visible as a cast that a reviewer can question.

Conversion Operator Workflow:

sequenceDiagram
    participant Code
    participant Compiler
    participant Op as Operator
    
    Code->>Compiler: double d = f;
    
    alt no explicit
        Compiler->>Op: implicit call
        Op->>Code: return double
        Note over Code: OK
    else with explicit
        Compiler->>Compiler: block implicit
        Compiler->>Code: compile error
    end
    
    Code->>Compiler: static_cast
    Compiler->>Op: explicit call
    Op->>Code: return double
    Note over Code: always OK

Example 3: bool Conversion

class SmartPointer {
    int* ptr;
    
public:
    explicit SmartPointer(int* p) : ptr(p) {}
    
    // ✅ Explicit bool
    explicit operator bool() const {
        return ptr != nullptr;
    }
};

int main() {
    SmartPointer sp(new int(10));
    
    if (sp) {  // OK: Allowed in conditional statements
        std::cout << "Valid" << std::endl;
    }
    
    // bool b = sp;  // Error
    bool b = static_cast<bool>(sp);  // OK
}

This works because the standard defines a special category called contextual conversion to bool. In the condition of if, while, for, the operands of !, &&, ||, the first operand of ?:, and static_assert/noexcept expressions, the compiler performs a direct initialization bool t(e); — and direct initialization is allowed to use explicit conversion functions. Everywhere else (assignment to a bool, passing to a bool parameter, returning from a function declared bool) the conversion is implicit and therefore rejected. That last case surprises people: bool isValid() const { return sp; } fails to compile and needs return static_cast<bool>(sp); or return sp != nullptr;-style code.

Before C++11, library authors used the “safe bool idiom” (returning a pointer-to-member type) to get the if (obj) behavior without enabling int x = obj;. explicit operator bool replaces that trick completely, and it is what std::unique_ptr, std::shared_ptr, std::optional, and std::function all use today.

Example 4: Copy Constructor

class Widget {
public:
    Widget() = default;
    
    // Explicit Copy Constructor (Rare)
    explicit Widget(const Widget& other) {
        // ...
    }
};

Widget w1;
// Widget w2 = w1;  // Error
Widget w2(w1);  // OK

This example exists mainly to show why you should not do it. An explicit copy constructor breaks far more than Widget w2 = w1;: passing a Widget by value to a function and returning one by value are both copy-initialization, so void f(Widget); can no longer be called with an lvalue, and many standard containers and algorithms that copy elements stop compiling. If you want to make copying deliberate, the usual alternatives are deleting the copy constructor and offering a named clone() member, or making the type move-only.

C++11 explicit Extensions

explicit Support by C++ Version

FeatureC++98C++11C++20
Constructor✅✅✅
Conversion Operator❌✅✅
Conditional explicit❌❌✅ explicit(bool)
Multi-Argument Constructor❌✅ (Brace Initialization)✅
// C++11: explicit for Conversion Operators
class MyClass {
public:
    explicit operator int() const {
        return 42;
    }
};

MyClass obj;
// int x = obj;  // Error
int x = static_cast<int>(obj);  // OK

C++20 Conditional explicit

template<typename T>
class Optional {
public:
    // Conditional explicit
    template<typename U>
    explicit(!std::is_convertible_v<U, T>)
    Optional(U&& value) : data(std::forward<U>(value)) {}
    
private:
    T data;
};

// int -> long is convertible → Not explicit
Optional<long> opt1 = 10;  // OK

// string -> int is not convertible → explicit
// Optional<int> opt2 = std::string("10");  // Error

explicit(bool) exists to solve a problem in generic wrappers. A wrapper like std::pair, std::tuple, or std::optional wants to be implicitly constructible from U exactly when T itself is implicitly constructible from U — so that std::optional<std::string> o = "text"; works but std::optional<std::vector<int>> v = 5; does not. Before C++20, the standard library achieved this by writing every such constructor twice, once explicit and once not, each constrained with SFINAE on std::is_convertible. With explicit(!std::is_convertible_v<U, T>) one declaration expresses the same rule, and the condition is evaluated per instantiation. The takeaway for your own templates: if a wrapper forwards a constructor, forward its explicitness too, or you will make the wrapper either more permissive or more annoying than the type it wraps.

Common Issues

Issue 1: Unintended Conversion

graph LR
    A[process 10 Call] --> B{String int Constructor}
    B -->|Without explicit| C[int → String Implicit Conversion]
    C --> D[Temporary String 10 Created]
    D --> E[Function Execution]
    E --> F[⚠️ Potential Bug]
    
    B -->|With explicit| G[❌ Compile Error]
    G --> H[Developer Expresses Intent Clearly]
    H --> I[process String 10]
    I --> J[✅ Safe Code]
// ❌ Without explicit
class String {
public:
    String(int size) {}
};

void process(String s) {}

process(10);  // Unintended conversion

// ✅ With explicit
class String {
public:
    explicit String(int size) {}
};

Real-World Examples:

ScenarioWithout explicitWith explicit
process(10)Creates String(10) and passes itCompile Error
String s = 10Creates String(10)Compile Error
String s(10)Creates String(10)Creates String(10)
return 10Returns String(10)Compile Error

Issue 2: Copy Initialization

class Widget {
public:
    explicit Widget(int x) {}
};

// Widget w = 10;  // Error
Widget w(10);  // OK
Widget w{10};  // OK (C++11)

Issue 3: Function Arguments

void func(std::vector<int> vec) {}

// ❌ Without explicit
// func(10);  // int -> vector (unintended)

// std::vector constructor is explicit
func(std::vector<int>(10));  // OK

Issue 4: bool Conversion

class Pointer {
public:
    operator bool() const {  // Without explicit
        return ptr != nullptr;
    }
    
private:
    int* ptr;
};

Pointer p;
int x = p;  // Converts to bool, then to int (unintended)

// ✅ With explicit
explicit operator bool() const {}

bool Conversion Issue Diagram:

graph TD
    A[Pointer p] --> B{operator bool explicit?}
    
    B -->|No| C[int x = p]
    C --> D[Pointer → bool]
    D --> E[bool → int]
    E --> F[⚠️ x = 0 or 1]
    F --> G[Unintended Integer Conversion]
    
    B -->|Yes| H[int x = p]
    H --> I[❌ Compile Error]
    I --> J[if p is OK]
    I --> K[Explicit Cast Required]

Allowed bool Conversion Contexts:

Contextexplicit boolDescription
if (obj)✅ AllowedConditional statements
while (obj)✅ AllowedLoop conditions
obj && x✅ AllowedLogical operators
!obj✅ AllowedLogical NOT
bool b = obj❌ ErrorCopy initialization not allowed
int x = obj❌ ErrorInteger conversion not allowed

Recommendations for explicit Usage

explicit Decision Flow

graph TD
    A[Writing Constructor/Conversion Operator] --> B{Single-Argument Constructor?}
    B -->|Yes| C{Intended Implicit Conversion?}
    B -->|No| D{Conversion Operator?}
    
    C -->|No| E[Use explicit ✅]
    C -->|Yes| F[Can Omit explicit]
    
    D -->|Yes| G{bool Conversion?}
    D -->|No| H[explicit Not Needed]
    
    G -->|Yes| E
    G -->|No| C

Code Guidelines

// ✅ Recommended explicit Usage
// 1. Single-Argument Constructor
explicit MyClass(int x);

// 2. Conversion Operator
explicit operator int() const;

// 3. bool Conversion Operator (Mandatory)
explicit operator bool() const;

// ❌ explicit Not Needed
// 1. Multi-Argument Constructor
MyClass(int x, int y);  // Only via braces: f({1, 2}) still converts implicitly

// 2. Copy/Move Constructor
MyClass(const MyClass&);  // Rarely explicit

// 3. Default Constructor
MyClass();  // No arguments

A note on the “not needed” list: since C++11, a multi-argument constructor can be used implicitly through copy-list-initialization, so void draw(Point p); draw({3, 4}); works when Point(int, int) is not explicit. For value-like types such as points, sizes, and colors that is usually exactly what you want, which is why the default recommendation is to leave multi-argument constructors non-explicit. If a braced list could plausibly mean something else — for example Timeout({5, 0}), where a reader can’t tell seconds from milliseconds — mark it explicit too. Also watch constructors whose extra parameters have default arguments: Array(int size, int fill = 0) is callable with one argument, so it is a converting constructor and needs explicit just like Array(int).

The broader guideline most style guides (the C++ Core Guidelines rule C.46, Google’s style guide) converge on is: make single-argument constructors explicit by default, and remove explicit only when the conversion is lossless and the two types genuinely represent the same value — std::string from const char* or std::chrono::duration between compatible units are the canonical examples.

Quick Reference by Constructor Type

ScenarioUse explicitReason
Vector(size_t size)✅ RequiredPrevent unintended size conversions
String(const char*)❌ OptionalString literal conversion is natural
operator bool()✅ RequiredPrevent unintended integer conversions
operator int()✅ RecommendedPrevent unintended arithmetic operations
Widget(int, int)❌ Not NeededMulti-argument constructors don’t allow implicit conversions
unique_ptr(T* ptr)✅ RequiredPrevent automatic pointer conversions

FAQ

Q1: When should I use explicit?

A:

  • Single-argument constructors
  • Conversion operators
  • To prevent unintended conversions

Q2: Does explicit affect performance?

A: No. It is a compile-time check; the generated code for process(Array(10)) and the old implicit process(10) is identical.

Q3: What about copy constructors?

A: Rarely explicit. Only in special cases.

Q4: What changed in C++11?

A: explicit can now be used with conversion operators.

Q5: What about bool conversions?

A: explicit is recommended to prevent unintended conversions.

Q6: Where can I learn more about explicit?

A:

  • “Effective C++”
  • “C++ Primer”
  • cppreference.com

Related Posts: Type Conversion, Copy Initialization, Smart Pointers.