explicit Constructors and Conversion Operators
C++ explicit keyword — preventing implicit conversions through constructors and conversion operators, plus conditional explicit(bool) in C++20.
explicitsince C++98The explicit specifier prevents a constructor or conversion operator from being used as an implicit conversion, requiring the programmer to write an explicit construction or cast at the call site.
Overview
Without explicit, a single-argument constructor doubles as an implicit conversion operator. The compiler silently invokes it whenever a value of the argument type appears where the class type is expected. This is occasionally useful (e.g. std::complex from a numeric scalar) but is almost always a source of bugs.
The explicit keyword has been extended twice since its C++98 debut:
- C++98 — applies to constructors; only single-argument constructors were called "converting constructors" at the time.
- C++11 — extends
explicitto conversion operators (operator T()); also clarifies that multi-argument constructors can beexplicitto block aggregate-style copy-list-initialization. - C++20 — introduces
explicit(bool), allowing the specifier to be conditional on a compile-time expression.
Syntax
// C++98: explicit constructor
struct Foo {
explicit Foo(int x);
explicit Foo(int x, int y); // multi-arg: blocks copy-list-init (C++11 semantics)
};
// C++11: explicit conversion operator
struct Bar {
explicit operator bool() const;
explicit operator std::string() const;
};
// C++20: conditional explicit
template<typename T>
struct Wrapper {
template<typename U>
explicit(!std::is_convertible_v<U, T>) Wrapper(U&&); // C++20
};Examples
The canonical bug that explicit prevents
struct FileDescriptor {
FileDescriptor(int fd) : fd_{fd} {} // implicit — dangerous
int fd_;
};
void read_from(FileDescriptor fd);
read_from(0); // Compiles silently. Passes literal 0 as fd — almost certainly a bug.
read_from(-1); // Also compiles. Negative fd passed to read: undefined behaviour territory.Mark the constructor explicit and the bug becomes a compile error:
struct FileDescriptor {
explicit FileDescriptor(int fd) : fd_{fd} {}
int fd_;
};
read_from(FileDescriptor{real_fd}); // intent is now visible at the call siteCopy-initialization vs. direct-initialization
These two forms of initialization are not equivalent when explicit is involved:
struct Metres {
explicit Metres(double d) : d_{d} {}
double d_;
};
Metres m1{3.5}; // OK: direct-list-initialization ignores explicit
Metres m2(3.5); // OK: direct-initialization ignores explicit
Metres m3 = 3.5; // ERROR: copy-initialization requires non-explicit constructor
Metres m4 = Metres{3.5}; // OK: explicit construction on the right-hand sideThe same asymmetry applies to function arguments:
void move_to(Metres distance);
move_to(3.5); // ERROR
move_to(Metres{3.5}); // OKMulti-argument constructors and list-initialization (C++11)
Prior to C++11, explicit on multi-argument constructors had no practical effect (they were never converting constructors). Since C++11, marking a multi-argument constructor explicit blocks copy-list-initialization:
struct Point {
explicit Point(int x, int y) : x_{x}, y_{y} {}
int x_, y_;
};
Point p1{1, 2}; // OK: direct-list-initialization
Point p2(1, 2); // OK: direct-initialization
Point p3 = {1, 2}; // ERROR: copy-list-init is blocked by explicit // C++11This matters for functions that accept Point by value and for aggregate-style APIs.
Explicit conversion operator (C++11)
Before C++11, operator bool() was notoriously dangerous because it silently enabled arithmetic:
// Pre-C++11 pattern — do not use
struct OldStream {
operator bool() const { return good_; } // implicit
bool good_ = true;
};
OldStream s;
int n = s + 1; // Compiles! OldStream → bool → int → int + 1. Completely wrong.explicit operator bool() (C++11) closes this hole. The conversion fires in exactly the contexts where a bool is unambiguously required — if, while, ?:, !, &&, || — but not in arithmetic:
struct Stream {
explicit operator bool() const { return good_; } // C++11
bool good_ = true;
};
Stream s;
if (s) {} // OK: contextual conversion to bool
bool b = s; // ERROR: implicit conversion
bool b2 = (bool)s; // OK: explicit cast
int n = s + 1; // ERROR: no implicit path to intThe standard library adopted this pattern for std::unique_ptr, std::shared_ptr, std::optional, std::expected (C++23), and all stream types.
explicit(bool) — conditional explicitness (C++20)
C++20 generalises the specifier to accept a constant expression. The primary use case is wrapper types that should mirror the convertibility of the wrapped type:
// A pair that is implicitly constructible iff both members are implicitly constructible // C++20
template<typename T, typename U>
struct Pair {
template<typename T2, typename U2>
explicit(!std::is_convertible_v<T2, T> || !std::is_convertible_v<U2, U>)
Pair(T2&& t, U2&& u)
: first{std::forward<T2>(t)}, second{std::forward<U2>(u)} {}
T first;
U second;
};
Pair<int, double> p1 = {1, 2.5}; // OK: both conversions are implicit
// Pair<Metres, double> p2 = {3.5, 2.5}; // ERROR: Metres is explicit — mirrored correctly
Pair<Metres, double> p3{{Metres{3.5}}, 2.5}; // OK: explicit constructionstd::pair and std::tuple use exactly this technique since C++20 to propagate the convertibility of their element types.
Best Practices
Default to explicit on all constructors callable with a single argument. Include constructors with one parameter, and constructors where all parameters beyond the first have defaults. Implicit conversion is an opt-in, not a default.
Always use explicit operator bool(). There is no legitimate use of an implicit operator bool() in modern C++. The standard library's pre-C++11 operator void*() hack existed precisely because implicit operator bool() was too dangerous.
Use explicit(bool) in generic wrappers when the wrapper is intended to transparently proxy another type's convertibility. Without it, a wrapper type is always either too strict (always explicit) or too permissive (always implicit).
Omit explicit intentionally in the narrow cases where implicit conversion models the domain: unit-less numeric wrappers like std::complex<double>, string-like types accepting const char*, or types that are meant to act as transparent adapters.
Common Pitfalls
Forgetting constructors with defaulted arguments. A constructor Foo(int x, bool flag = false) is a converting constructor callable with a single int. It needs explicit just as much as Foo(int x).
Confusing direct-initialization and copy-initialization. T t = expr is copy-initialization and rejects explicit constructors. T t{expr} and T t(expr) are direct-initialization and accept them. This surprises engineers who expect = to be purely syntactic sugar.
Marking copy and move constructors explicit. Technically legal, but it disables almost all ordinary use of the class — assignment from temporaries, returning by value, passing to functions. Only do this intentionally for non-copyable sentinel types, and document why.
explicit does not affect static_cast or C-style casts. It only blocks implicit sequences. A caller can always bypass it with an explicit cast, which is the intended design — the keyword is about preventing accidents, not enforcing encapsulation.
Forgetting explicit on operator bool() in template base classes. When a CRTP base provides operator bool(), a derived class that forgets to redeclare it as explicit silently inherits the implicit form (if the base's version is not explicit). Each class in the hierarchy needs its own explicit operator bool() or a non-virtual explicit version in the base that derived classes cannot override implicitly.
Explicit Conversion Operators In Depth
Conversion operators deserve more attention than they typically get. Before C++11, every user-defined conversion operator was implicit. The only way to have a safe truthiness test was the Safe Bool Idiom — a clever abuse of pointer-to-member types whose arithmetic rules prevented the worst promotions:
// Pre-C++11 safe bool (historical reference — do NOT use in modern code)
class OldHandle {
private:
typedef void (OldHandle::*SafeBool)() const;
void safe_bool_helper() const {}
public:
// Returns pointer-to-member (non-null if valid); immune to arithmetic
operator SafeBool() const {
return valid_ ? &OldHandle::safe_bool_helper : nullptr;
}
private:
bool valid_ = true;
};This pattern existed in Qt (QScopedPointer::RestrictedBool), Boost.SmartPtr, and many other libraries. It was retired once explicit operator bool() arrived in C++11.
What contexts trigger a contextual conversion to bool?
explicit operator bool() fires in contextual conversion to bool contexts. The standard lists these as:
- Conditions of
if,while,do/while, andforstatements - The condition of the ternary operator
?: - Operands of the logical operators
&&,||, and! - The first argument of
static_assert - The operand of
noexcept(when used as an operator) - Predicate positions in many standard-library algorithms
Notably, return statements are not contextual conversions. return my_obj; where the return type is bool uses implicit conversion, not contextual. This can surprise callers who try to return a type with only explicit operator bool():
struct Handle {
explicit operator bool() const { return valid_; }
bool valid_ = true;
};
// This fails to compile — return is an implicit conversion:
// bool check(Handle h) { return h; } // ERROR
// Correct forms:
bool check(Handle h) { return static_cast<bool>(h); } // explicit cast
bool check2(Handle h) { return bool{h}; } // direct-init
bool check3(Handle h) { return !!h; } // double-negation (idiomatic)Explicit conversion to types other than bool
explicit works on any conversion operator, not just operator bool(). This is useful when implicit narrowing or lossy conversion would be surprising:
struct Rational {
int num, den;
// Allow explicit-only extraction of the approximate double value
explicit operator double() const {
return static_cast<double>(num) / den;
}
// Allow explicit-only truncation to int
explicit operator int() const { return num / den; }
};
Rational r{3, 4};
double d = r; // ERROR: implicit conversion disabled
double d2 = (double)r; // OK: explicit cast
double d3 = static_cast<double>(r); // OK
// Still usable in direct-init contexts:
double d4{r}; // OK: direct-list-initExplicit Deduction Guides (C++17 CTAD)
Class Template Argument Deduction (CTAD), introduced in C++17, lets the compiler deduce template arguments from constructor call sites. Deduction guides are hints that tell the compiler how to perform that deduction. Like constructors, a deduction guide can be marked explicit.
Syntax of a deduction guide
// Non-explicit guide: allows implicit deduction in copy-init context
template<class Iter>
container(Iter b, Iter e)
-> container<typename std::iterator_traits<Iter>::value_type>;
// Explicit guide: only participates in direct-initialization deduction
template<class Iter>
explicit container(Iter b, Iter e)
-> container<typename std::iterator_traits<Iter>::value_type>;How explicit on a deduction guide affects copy-initialization
An explicit deduction guide is ignored during copy-list-initialization (= {...} syntax), exactly as an explicit constructor is:
template<class T>
struct Box {
explicit Box(T const& t) : val{t} {}
T val;
};
// Explicit constructor generates an explicit implicit deduction guide.
// The copy deduction candidate (from the copy constructor) is non-explicit.
Box b1{42}; // OK: direct-list-init — explicit guide participates // C++17
Box b2(42); // OK: direct-init
Box b3 = {42}; // ERROR: copy-list-init, explicit guide ignored, no match
Box b4 = Box{42}; // OK: explicit construction on the right-hand sideWhen you write a user-defined deduction guide marked explicit, you get the same behaviour: the guide is excluded from overload resolution whenever copy-initialization is in play:
template<class T>
struct Wrapper {
T data;
};
// User-defined explicit deduction guide
template<class T>
explicit Wrapper(T) -> Wrapper<T>;
Wrapper w1{42}; // OK: direct-list-init // C++17
Wrapper w2 = {42}; // ERROR: explicit guide not considered for copy-list-initThis is particularly useful when you want CTAD to work in "deliberate" contexts but not leak into implicit conversion chains — for instance, a buffer wrapper that should be constructed explicitly from a raw pointer but never silently created from one.
Decision Table: When to Mark a Constructor explicit
| Situation | Use explicit? | Rationale |
|---|---|---|
| Single-argument constructor (any type) | Yes — always by default | Prevents accidental implicit conversion |
| Constructor with defaulted args making it callable with 1 arg | Yes | Same risk as single-arg; easily overlooked |
operator bool() | Yes — always | The pre-C++11 implicit version is never correct in modern code |
Conversion to a non-bool type (e.g. operator double()) | Yes, unless conversion is lossless and domain-obvious | Lossy or surprising conversions should require opt-in |
| Copy constructor / move constructor | Only deliberately | Making them explicit breaks almost all ordinary use of the type |
| Multi-argument constructor | Yes if = {a, b} syntax should be blocked | Blocks copy-list-initialization; use when aggregate-style creation should be opt-in |
| Generic wrapper mirroring inner-type convertibility | Use explicit(bool) instead | Conditional explicit propagates inner-type semantics cleanly |
| Domain type where implicit conversion models the domain well | No (intentional omission) | e.g. std::complex<double> from a scalar; std::string from const char* |
explicit(bool) — The C++20 SFINAE Replacement
The problem it solves
Before C++20, making a wrapper conditionally explicit required two separate constructor overloads gated by std::enable_if_t:
// C++17 — verbose and doubles compile-time work
template<class T>
struct Wrapper {
template<class U,
std::enable_if_t<std::is_convertible_v<U, T>>* = nullptr>
Wrapper(U const& u) : val{u} {} // implicit overload
template<class U,
std::enable_if_t<!std::is_convertible_v<U, T>>* = nullptr>
explicit Wrapper(U const& u) : val{u} {} // explicit overload
T val;
};The C++20 solution
explicit(bool) (P0892R2) condenses this to a single constructor. The argument is a constant expression evaluated at instantiation time:
// C++20 — one constructor, conditional explicit
template<class T>
struct Wrapper {
template<class U>
explicit(!std::is_convertible_v<U, T>)
Wrapper(U const& u) : val{u} {}
T val;
};explicit(true) is equivalent to plain explicit. explicit(false) is equivalent to omitting the specifier. The standard library uses this pattern for std::pair and std::tuple since C++20.
std::pair and std::tuple — the canonical use case
std::string has an explicit constructor from std::string_view. Before C++20, a std::pair<std::string, int> could sometimes be constructed implicitly from {"hello"sv, 1} depending on which overload the implementation picked. C++20 fixed this by making pair and tuple use explicit(!is_convertible_v<...>):
// std::pair (simplified C++20 implementation sketch)
template<class T1, class T2>
struct pair {
template<class U1, class U2>
explicit(!std::is_convertible_v<U1, T1> ||
!std::is_convertible_v<U2, T2>) // C++20
pair(U1&& u1, U2&& u2)
: first{std::forward<U1>(u1)}, second{std::forward<U2>(u2)} {}
T1 first;
T2 second;
};
// With int and double — both implicitly constructible — the pair is implicit:
std::pair<int, double> p1 = {1, 2.5}; // OK
// With string_view→string requiring explicit construction, the pair mirrors it:
// std::pair<std::string, int> p2 = {"hello"sv, 1}; // ERROR in C++20
std::pair<std::string, int> p3{"hello"sv, 1}; // OK: direct-initstd::tuple applies the same rule: the tuple constructor is explicit if and only if at least one element type does not support implicit conversion from the corresponding argument.
Compile-time cost benefit
Benchmarks with 500 template instantiations showed that explicit(bool) is approximately 15% faster than the two-overload SFINAE approach at the compiler frontend (measured in MSVC 2019). This is because two overloads means two instantiation candidates and two substitutions; one conditional overload means one.
explicit(bool) on conversion operators
The specifier works on conversion operators too:
template<class T>
struct MaybeValue {
T val;
bool has_value;
// Explicit iff T is not trivially convertible to bool
explicit(!std::is_same_v<T, bool>)
operator bool() const { return has_value && static_cast<bool>(val); }
explicit(!std::is_convertible_v<T, double>)
operator double() const { return static_cast<double>(val); }
};See Also
- Constructors — constructor syntax, delegation, and initialization order
- Type Conversions and Casts —
static_cast,reinterpret_cast, implicit conversion rules - CRTP — pattern where
explicit operator bool()inheritance issues commonly appear - std::optional — canonical use of
explicit operator bool()in the standard library