Forth to the past: C++ “features” that make life hard

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.

Posted in

Leave a Reply

Your email address will not be published. Required fields are marked *