55 Intermediate The Hundred
The Constructor That Was a Conversion
a constructor with an open-door policy
#include <iostream>
struct Grid {
int rows, cols;
Grid(int r, int c) : rows(r), cols(c) {}
Grid(int n) : rows(n), cols(n) {} // square shortcut
};
void render(const Grid& g) {
std::cout << "rendered a " << g.rows << "x" << g.cols << " grid\n";
}
int main() {
Grid board(3, 4);
render(board);
render(5); // left over from the old int-based render(int)
}
Run it. What does it print?
Answer
rendered a 3x4 grid, then rendered a 5x5 grid — the 5 became a grid nobody asked for.
Why¶
A constructor callable with one argument is not just a constructor: it is a standing offer
to convert that argument type into your class, anywhere in the program. Overload resolution
sees render(5), finds no render(int), spots Grid(int) and inserts one user-defined
conversion — a temporary 5x5 Grid that lives just long enough for the call. The same
open door makes Grid g = 5; legal and board = 9; too, so a typo can swap your 3x4 board
for a 9x9 one. Nothing here troubles the compiler: -Wall -Wextra -Wconversion stays quiet.
Since C++11 even multi-argument constructors convert from a braced list, so render({3, 4})
compiles too.
The fix¶
One keyword revokes the offer:
Now the stale call is a compile error instead of a wrong picture:
error: invalid initialization of reference of type ‘const Grid&’ from expression of type ‘int’
note: in passing argument 1 of ‘void render(const Grid&)’
Deliberate construction still works — render(Grid{5}) prints rendered a 5x5 grid; you
just have to ask out loud. Conversion operators deserve the same guard: an explicit
operator bool() still works in if (h) but stops h + 1 from quietly compiling.
Takeaway: a one-argument constructor is a conversion offer to the whole program — say
explicit unless you mean to make it.
Try it: g++ -std=c++17 main.cpp -o demo && ./demo — then add explicit to Grid(int).