This is a continuation of the C++ series, previous post here. When I was working on the move semantics example, I ran into two familiar, yet forgotten “features” of C++ that caused me major pain:
- Template code is not verified for correctness unless instantiated.
- Any single-parameter constructor creates an implicit conversion.
These annoyances are not created equal: the former is much, much more painful than the latter. Modern C++ is all about templates, but you can write any old nonsense in a template: as long as it can pass the lexer, the template will compile. The real correctness check happens when (and if) the tempalte is instantiated, and in C++ it is often hard to tell when it happens and whether it happens at all, especially if we are talking about a nameless method like a constructor or an assignment operator. In practice this means that it is entirely possible to write a method with a typo, have a program compile and even run, and then discover the error days, weeks, or even months later.
For instance, if I go to matrix.h in my example and replace assignment operator of the matrix
class with the following “code”
matrix& operator=(matrix const& other) { <div style="color:red;font-size:15px"> FREE PIZZA FOR ALL!!! </div> }
the program still happily compiles. Assignment operator for matrices is not instantiated. Some rudimental checks are done, e.g. &nsbp;
compiles, but @override
does not: ‘@’ is an illegal character. I am not sure what are the rules exactly (the C++ standard costs top dollar), but anyway, the problem is not limited to typos: you can write code that could not be valid C++ under any circumstances, and it will compile.
Implicit conversions are less frequent, but sometimes more baffling. I have class logger
with a constructor along these lines:
class logger { public: logger(std::ostream& stream) : n_indent(0), m_stream(stream) {} ... }
I then have two operators:
std::ostream& operator<<(std::ostream& stream, logger const& logger); template<typename T> std::ostream& operator<<(logger const& logger, T&& value);
If I write something like
logger x(cout); cout << x;
I get a compilation error: two operators above are in conflict. The first one is applicable, but involves an implicit conversion of the right operand from logger
to logger const&
, and the second one is applicable, but involves an implicit conversion of the left operand from ostream
to logger
. C++ cannot choose between the two, and thus complains.
When I defined a constructor for logger
, the last thing I thought of was implicit conversions. When I’ve got an ambiguity error, it took me a few moments to realize what is going on, and then I said ‘aha!’ and made the constructor explicit
. Problem solved.
BUT: turning any single-parameter constructor into an implicit conversion by default was a poor language design choice. I am sure if C++ were reworked from scracth, it would have left conversions explicit by default and would have had keyword implicit
to override that. But C++ is not being reworked from scratch, and thus we get those potential land mines every time we absent-mindedly define a single-parameter constructor and forget to mark it explicit
. Not cool.
To conclude: I am not talking here about some abstract impurities that may make one’s life difficult in theory. These two actually did make my life difficult in the course of a few hours that took me to create the example.