C++ Part I · Module 12 · Constructors and Destructors
explicit and Unwanted Implicit Conversions
A constructor taking a single argument does something beyond constructing. It teaches the compiler a conversion — from that argument's type into yours — and the compiler will then apply it wherever a value of your type is wanted and a value of the argument's type is present. That is occasionally the whole point, which is why std::string accepts a string literal without ceremony and why nobody would want it otherwise. It is also, far more often, a silent trap, because the conversion applies in places you were not thinking about and leaves no trace at the call site for a reader to notice. The measurement in this chapter is a function that takes a buffer by const reference, called with an integer count by someone who meant to pass a buffer: it compiled with no diagnostic of any kind, constructed a buffer whose capacity was that count, and processed it. One keyword on the constructor turns that into a compile error, and the error is a good one — it names the conversion that is no longer available. The rest of the chapter is the precise boundary of what the keyword blocks, because it does not block everything and the distinction it draws is a sensible one once you can see it.
Intermediate16 min readC++Part IFundamentalsConstructors
Part I · Module 12 · Chapter 12.5 · explicit and Unwanted Implicit Conversions
1. A One-Argument Constructor Is a Conversion
class Width {
public:
Width(int bits) : bits_(bits) {}
private:
int bits_;
};That declares two things. The obvious one is how to build a Width from an int. The other is that an int can become a Width wherever one is needed — and the compiler will do it without being asked.
void takesWidth(Width w);
takesWidth(8); // compiles: 8 becomes a Width2. The Hazard, Measured
#include <cstdio>
class Buffer {
public:
Buffer(int capacity) : cap_(capacity) {} // NOT explicit
int capacity() const { return cap_; }
private:
int cap_;
};
static void process(const Buffer& b)
{
std::printf(" processing a buffer of capacity %d\n", b.capacity());
}
int main() {
int sampleCount = 4096;
process(sampleCount); // meant process(someBuffer); compiles anyway
return 0;
}diagnostics: 0
processing a buffer of capacity 4096Zero diagnostics. The author meant to pass a buffer and passed a count. The compiler built a Buffer with capacity 4096 out of it, process ran, and nothing anywhere suggested a problem.
Three things make this worse than it first looks:
There is no cast, no conversion, nothing to see at the call site. process(sampleCount) looks like a correct call.
The constructed object was a temporary bound to the const Buffer& — Chapter 6.5 §5 established that a const reference binds to a temporary and Chapter 8.8 §6 measured the lifetime extension. So the mechanism that makes const& parameters convenient is also what lets this through.
And it would have been caught by a type error otherwise. The whole value of having a Buffer type rather than passing an int capacity around — Chapter 9.11 §9's a type is a restriction and the restriction is the product — was just undone by the constructor.
Now the same program with one keyword added:
explicit Buffer(int capacity) : cap_(capacity) {}error: no matching function for call to 'process'
note: candidate function not viable: no known conversion from 'int' to 'const Buffer' for 1st argumentThe note is the useful half: it says exactly what is missing — a conversion from int to const Buffer — so a reader who genuinely wanted one knows what to write.
3. What explicit Means
explicitstops the constructor being used for an implicit conversion. The constructor still exists and can still be called directly.
So the type keeps its constructor and loses the hidden conversion.
4. The Exact Boundary
Measured, eight cases compiled one at a time, with W having an implicit constructor and O an explicit one:
| Code | Result |
|---|---|
takesW(8); | compiles |
takesO(8); | error: no matching function for call to 'takesO' |
W w = 32; | compiles |
O o = 32; | error: no viable conversion from 'int' to 'O' |
O o(32); | compiles |
O o{32}; | compiles |
takesO(O(8)); | compiles |
W w = {32}; | compiles |
The pattern is a clean two-way split:
Blocked — implicit conversion and copy-initialization. Passing a bare 8 to a function, and the = form O o = 32;. These are the cases where the conversion happens without anyone writing the type's name.
Allowed — direct initialization. O o(32) and O o{32} both work, and so does takesO(O(8)). These all name the type, so the conversion is visible in the source.
That is exactly the right line to draw, and it is the same principle as Chapter 4.10 §4's argument about casts: the operation is not forbidden, it is made visible. A reader of takesO(O(8)) can see a conversion happening; a reader of takesO(8) cannot.
(A method note: my first run of this table mislabelled four of the eight rows as refused, because I was testing for any compiler output rather than for errors specifically, and an unused helper function was producing -Wunused-function. The corrected run is above. A harness that cannot distinguish a warning from an error will tell you confidently wrong things — this is the third time in this curriculum that has caught me.)
5. When to Write It
Write
expliciton every constructor that can be called with one argument, unless you have a specific reason to want the conversion.
The rule is a default rather than an absolute, and the reason it is the right default is Chapter 11.4's callout argument about asymmetry, which applies here unchanged:
A missing explicit fails silently and far away. §2's measurement is the shape — the bug is at a call site in another file, there is nothing to see, and it compiles.
An unnecessary explicit fails immediately and locally. Some caller writes takesO(8), gets a clear error naming the missing conversion, and either writes takesO(O(8)) or you remove the keyword. Cost: seconds.
Note also that "can be called with one argument" is broader than "takes one parameter" — a constructor with defaults for all but the first qualifies:
explicit Reg(int addr, int width = 32); // callable as Reg(0x40)6. When You Genuinely Want the Conversion
Two cases, and both are real.
When your type is a better version of the argument's type. std::string from a const char* is the model case: the conversion is lossless, the meaning is unambiguous, and requiring std::string("idle") everywhere would be pure noise. Chapter 9.5's whole convenience depends on it.
When the type is a thin wrapper whose entire purpose is to carry the value. A Metres wrapping a double, where every double in context is a length. Even here it is worth pausing: Chapter 9.9 measured that the reason to have a distinct type at all is usually to stop values being confused, and an implicit conversion gives that back.
The test: would a reader of the call site be surprised? greet("world") with a std::string parameter surprises nobody. process(4096) with a Buffer parameter surprises everybody.
7. Multi-Argument Constructors, Measured
Section 2's hazard needed only one argument, which invites the conclusion that explicit is a single-argument concern. It is not, and the claim is worth measuring rather than asserting.
With Imp(int, int) implicit and Exp(int, int) explicit:
| Code | Result |
|---|---|
Imp i = {1,2}; | compiles |
Exp e = {1,2}; | error: chosen constructor is explicit in copy-init |
Exp e{1,2}; | compiles |
takesImp({1,2}); | compiles |
takesExp({1,2}); | error: no matching function for call to 'takesExp' |
takesExp(Exp{1,2}); | compiles |
The same two-way split as §4. A braced list is itself a way of writing a value without naming its type, so takesImp({1,2}) has exactly the shape of §2's bug — a call site where nothing indicates which type is being constructed. The error message for the copy-initialization case is unusually direct: chosen constructor is explicit in copy-init.
So the rule generalises:
explicitblocks every form where the type's name does not appear at the call site, regardless of how many arguments the constructor takes.
The practical consequence is a small amendment to §5's advice: write explicit on constructors callable with one argument and on multi-argument constructors whose braced form would be ambiguous at a call site. A Rect{0,0,100,100} passed to draw({0,0,100,100}) is readable only if there is exactly one thing it could be.
8. One Adjacent Fact
Conversions can run the other way too. A type can define an operator that converts it into something else, and that can be marked explicit as well — which is why if (stream) works while an accidental int n = stream; does not. That belongs with Chapter 15's operator overloading and is named here only so the symmetry is visible.
9. explicit and = delete Are Complementary
Chapter 12.4 §8 measured the other tool:
class Reg {
public:
explicit Reg(int) {}
Reg(double) = delete;
};They solve adjacent problems and you often want both:
| Tool | Stops | Measured |
|---|---|---|
explicit | an argument of the right type converting into your type silently | process(4096) → no known conversion from 'int' |
= delete on an overload | an argument of the wrong type being converted to the right one | Reg b(1.5) → call to deleted constructor |
Without explicit, process(4096) compiles. Without the deleted overload, Reg b(1.5) silently truncates 1.5 to 1 — Chapter 4.10's narrowing conversion. A type that wants to be strict about its inputs needs both.
10. What to Actually Do
Write explicit on every constructor callable with one argument unless you have a reason not to.
Include constructors with defaults for all but the first — they are callable with one argument.
Ask whether the two types are the same kind of thing, not merely whether a mistake is possible.
Leave it off when your type is a better representation of the argument's type, as std::string is for const char*.
Prefer a named constructor to an implicit conversion when you want brevity — Buffer::withCapacity(4096).
Pair it with = delete on overloads that should never be callable (Chapter 12.4 §8).
Read the note in the error — it tells a legitimate caller exactly what to write.
And remember direct initialization still works. explicit does not make the constructor awkward to use, only invisible to use.
11. Mistakes Worth Making Once
Omitting explicit on a single-argument constructor. Measured: process(4096) compiled, ran, and reported capacity 4096 with 0 diagnostics.
Assuming explicit blocks everything. O o(32) and O o{32} still compile — that is the design.
Thinking O o = 32; and O o(32); are the same. Measured: the first is refused, the second is fine.
Forgetting a defaulted-parameter constructor is single-argument callable.
Adding an implicit conversion for convenience between two types that exist to be distinguished — Celsius and Fahrenheit through double.
Relying on explicit to stop a wrong-typed argument. That is = delete's job; the two are complementary.
Judging a warning as an error in a test harness. My first run of §4's table mislabelled four rows.
12. Interview Relevance
"What does explicit do?" Stops a constructor being used for an implicit conversion and for copy-initialization, while leaving direct initialization available. Naming the boundary — blocked for f(8) and O o = 8, allowed for O o(8) — is the complete answer.
"When would you leave it off?" When the types are genuinely interchangeable, as std::string and const char* are. Candidates who say "always use explicit" have a good default and an incomplete understanding.
"Show the bug it prevents." §2's measurement is the ideal answer: a function taking const Buffer& called with an int, compiling silently. Being able to state that there is no diagnostic is what makes it convincing.
"Does it matter on a two-argument constructor?" Yes, for brace copy-initialization — a smaller issue, and knowing it exists is a good sign.
"How does it relate to = delete?" Complementary: explicit stops the right type converting in silently, a deleted overload stops the wrong type converting at all.
13. What You Should Be Able to Say Now
- A single-argument constructor declares a conversion from that argument's type into yours.
- Measured:
process(sampleCount)with aconst Buffer¶meter compiled with 0 diagnostics, built a buffer of capacity 4096, and ran. - With
explicit:error: no matching function for call to 'process'plusnote: … no known conversion from 'int' to 'const Buffer'. - The temporary was bound to the
const&— the same mechanism that makes Chapter 6.5's parameters convenient. explicitblocks implicit conversion and copy-initialization; it permits direct initialization.- Measured, eight cases:
takesO(8)andO o = 32refused;O o(32),O o{32}andtakesO(O(8))fine. - The principle is Chapter 4.10's: the conversion is made visible, not forbidden.
- It is not a single-argument concern. Measured:
Exp e = {1,2};giveschosen constructor is explicit in copy-init, andtakesExp({1,2})is refused, whileExp e{1,2}andtakesExp(Exp{1,2})compile. - A braced list is itself a way of writing a value without naming its type, which is why it has §2's shape.
- Write it by default — the omission fails silently in someone else's file; the inclusion fails immediately in yours.
- "Callable with one argument" includes constructors with defaults for the rest.
- Leave it off when the types are genuinely interchangeable, as for
std::stringfromconst char*. - The real question is whether the two types are the same kind of thing, not whether a mistake is possible.
explicitand= deleteare complementary — one stops the right type converting in, the other stops the wrong type converting at all.- A named constructor is usually the better way to buy brevity.
14. Exercises
Exercise 1 — add it. For class Celsius { Celsius(double); }; say whether to add explicit, and give the reasoning in terms of what other types exist in the codebase.
Exercise 2 — predict. With class E { public: explicit E(int); }; which compile: (a) E a(1); (b) E b = 1; (c) E c{1}; (d) void f(E); f(1); (e) void f(E); f(E(1)); (f) E d = E(1);
Exercise 3 — find the bug. void configure(const Timeout& t); with Timeout(int ms) not explicit. Give a call that compiles and is wrong, and say what the reader sees.
Exercise 4 — same kind of thing? Decide for each whether the conversion should be implicit: std::string from const char*, Buffer from int, Matrix from double, Handle from int, Fraction from int.
Exercise 5 — both tools. Write a RegisterAddress class that accepts only int, rejects double outright, and cannot be constructed implicitly. Say which keyword does which job.
Exercise 6 — the defaults trap. Say why Reg(int addr, int width = 32) needs explicit and what call it would otherwise permit.
Exercise 7 — multi-argument. For class Rect { Rect(int,int,int,int); }; say whether explicit matters, give a call site that would be unreadable without it, and name the error you would get with it.
Exercise 8 — brevity without the conversion. Rewrite a design that relies on an implicit conversion for terseness, using Chapter 12.2 §6's named constructors instead.
15. Summary
- A one-argument constructor is a conversion, applied wherever your type is wanted.
- Measured:
process(4096)compiled with 0 diagnostics and processed a buffer of capacity 4096. explicitturns that into an error whose note names the missing conversion.- It blocks implicit and copy-initialization and permits direct — measured across eight cases.
- The principle is visibility, not prohibition (Chapter 4.10 §4).
- Not a single-argument concern — measured:
Exp e = {1,2}is refused asexplicit in copy-init, and so istakesExp({1,2}). - Write it by default, because the omission fails silently and elsewhere.
- "Callable with one argument" includes defaulted parameters.
- Leave it off only when the types are genuinely interchangeable.
- The real question is whether they are the same kind of thing — a restriction is the product (Chapter 9.11 §9).
- Pair it with
= delete; prefer a named constructor when you want brevity.
Chapter 12.2 arrived at one constructor that checks and several named ways in, and left the mechanism connecting them unexamined — the factories in that chapter built their result by calling Field(firstBit, lastBit - firstBit + 1), which works for a static function returning an object but does not help a constructor that wants to reuse another constructor's work. Doing that requires a specific syntax, available since C++11, and it is the only way to route several constructors through one without duplicating the invariant check or inventing a private helper. The next chapter is that syntax, with the order of events measured — including what happens to the delegating constructor's own body, and confirmation that a check in the target still fires when the caller came in through a delegation.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
