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
| Case | Without explicit | With explicit |
|---|---|---|
| Copy Initialization | String s = 10; ✅ | String s = 10; ❌ |
| Direct Initialization | String s(10); ✅ | String s(10); ✅ |
| Brace Initialization | String s{10}; ✅ | String s{10}; ✅ |
| Function Argument | func(10); ✅ | func(10); ❌ |
| Return Value | return 10; ✅ | return 10; ❌ |
| static_cast | static_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
| Feature | C++98 | C++11 | C++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:
| Scenario | Without explicit | With explicit |
|---|---|---|
process(10) | Creates String(10) and passes it | Compile Error |
String s = 10 | Creates String(10) | Compile Error |
String s(10) | Creates String(10) | Creates String(10) |
return 10 | Returns 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:
| Context | explicit bool | Description |
|---|---|---|
if (obj) | ✅ Allowed | Conditional statements |
while (obj) | ✅ Allowed | Loop conditions |
obj && x | ✅ Allowed | Logical operators |
!obj | ✅ Allowed | Logical NOT |
bool b = obj | ❌ Error | Copy initialization not allowed |
int x = obj | ❌ Error | Integer 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
| Scenario | Use explicit | Reason |
|---|---|---|
Vector(size_t size) | ✅ Required | Prevent unintended size conversions |
String(const char*) | ❌ Optional | String literal conversion is natural |
operator bool() | ✅ Required | Prevent unintended integer conversions |
operator int() | ✅ Recommended | Prevent unintended arithmetic operations |
Widget(int, int) | ❌ Not Needed | Multi-argument constructors don’t allow implicit conversions |
unique_ptr(T* ptr) | ✅ Required | Prevent 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.
Related Articles
- C++ Type Conversion | Implicit, Explicit, and User-Defined
- C++ Copy vs Direct Initialization: Why T x = value Fails Where T x(value) Compiles
- C++ Copy & Move Constructors: Rule of Five and RAII
- C++ Smart Pointers