I have worked on C++ for most of my professional career. Somehow, I have gotten more intimidated by the language the more I have learnt it. The language is very powerful but the power comes with a lot of emergent complexity that makes the language hard to contain. It feels like no individual even understands the full language. But individuals are supposed to read and write code in C++.
The complexity was the most obvious to me when I worked in Developer Experience and had to deal with compiler bugs that were impacting the whole company.
Temporary Lifetime Extension
In C++, you have to think about the lifetime of variables, particularly when taking a reference to that variable. So, for example, doing this would be unsafe as you would return a reference to a variable that gets cleaned up at the end of the function:
int& BadFunction()
{
int x = 0;
return x;
}
Another variation of this same problem is to take the reference of a temporary expression, like a function call.
int& i_am_a_ref = return_a_value()
The value returned by return_a_value
is destroyed after the expression is evaluated. So i_am_a_ref
is holding a reference to something that no longer exists. This is not allowed and normally a compiler will error out if you try to do this.
This was my understanding as well till one day we upgraded our C++ compiler. The new compiler version didn’t cause any compiler errors but resulted in crashes in production. Eventually, someone found the culprit in a line of code that looks like this:
const int& i_am_a_ref = return_a_value()
Knowing what we know, that is obviously wrong code. So I figured, we had just gotten lucky with previous compilers and are finally paying the price of our misuse of the language.
Then someone said the magic words: temporary lifetime extension. Apparently, the existence of that const
makes that code valid. The compiler is supposed to see the const
and extend the lifetime of the temporary variable so it’s alive for the whole local scope which makes the reference valid as well.
Seriously, why?
In 1992, Bell Labs published a paper talking about temporaries and whether their lifetime should be the whole block or if it should be just the statement. By 1993, it seems to have been decided that temporaries should be destroyed by the end of the statement.
Then a few months later, this paper advocates for adding an exception and extending the lifetime of the temporary when assigned to a const reference. The paper highlights similar problems to the Bell Labs’ paper, but is only focused on references while the Bell Labs’ paper focused on pointers. I don’t see why there should be a distinction, but regardless, this distinction was made.
The paper advocating for this exception is kinda hilarious because while advocating for it, the author agrees that the option also sucks:
Options:
- Eliminate the ability to explicitly bind to a temporary. This would be ok with me, since I see this construct as fraught with peril. Unfortunately, it is a part of the language, and I doubt that removing it now is an option.
- Extend the lifetime of the temporaries involved.
Regardless of how the decision was made, this feature has been part of C++ since 1993. But somehow, despite being a user of C++ for a long time, I had never heard of it.
Fortunately, the company I worked for had many people more proficient with C++, particularly with the nuances of the C++ standard. They were the ones who recognized the potential lifetime extension. However, since the original code was more complicated than what I presented here, it still took these experts a majority of a day to debate whether this was an actual compiler bug or if there were other rules of the language that allowed the compiler to not extend the lifetime of the variable.
Once we were sure that this was indeed a compiler bug, we were able to flag it to our vendor and get the bug fixed (but only after their language experts spent time debating whether this was a compiler bug or just a change-in-behavior).
Designated Initializers
People usually think of C++ as C with classes. Of course, C++ has added A LOT more features other than classes. But still, people reasonably think of C++ as a superset of C. Unfortunately, the two languages are evolving independently and keep accruing incompatible features. It’s crazy that we let this happen but it’s no longer reasonable to assume that you can just compile your C code as C++.
For example, in C99 you can initialize an array by providing explicit indices like this:
int array[4] = { [3] = 99, [1] = 89 } // [0, 89, 0 99]
This, and similar syntax for initializing structs, are called designated initializers. C++ did not have any similar features for a long time till they decided to add some in C++20. But instead of being at parity with the C syntax, they chose to implement their own thing and decided to not include the array initialization example above. So the code above is invalid C++.
Okay so that’s annoying, but what’s the compiler bug? Well, in GCC7, if that code is compiled as C++, then the compiler will silently ignore the indices and just construct the wrong array!
// In GCC7
int array[4] = { [3] = 99, [1] = 89 } // [ 99, 89, 0, 0 ]
We apparently were running this bad code for years. Only when we started testing GCC9 did we find the issue as GCC9 properly flagged it as invalid C++ code. But, of course, when we found the compiler error we had to spend a day arguing whether the compiler was right or not because when we first searched for this syntax we found C documentation and just assumed that it would be valid C++ as well.
Template Disambiguator
Templates, in C++, allow for generics and a lot of compile-time metaprogramming. The complexity of templates is usually the first thing people think of when they think of C++ code that is difficult to understand or debug.
Sometimes nested templates can get so complex that the template-ness of methods can get lost. So to make sure the compiler can “remember” that a method is a template method, you have to “disambiguate” it for the compiler. You do this by typing (and I can’t believe this) template<SPACE>
before you invoke the method on an object. So the final call should look like this:
obj.template member_func<Type>();
That doesn’t even look like C++. Why is there a space there!
Matter of fact, even if you aren’t using nested templates and you just want to throw a template<SPACE>
in there for fun, you could do that. GCC, for example, will just ignore the unnecessary disambiguation.
Clang, though, doesn’t like the unnecessary disambiguation and will fail to compile the program. This mismatch between the two compilers has existed for years1.
The way we found this issue is that we use both compilers. We build our production code using GCC because it’s officially supported by our OS/platform vendors. But for all our custom static analysis tooling, we relied on the clang toolchain. So any codebase that used an unnecessary template disambiguation would cause a problem. The code would build and run fine with GCC but it would fail with any of our custom tools.
By the time I had found out about this issue, someone else had already done the full research, but I like to imagine my reaction if I had to investigate the failures with our toolchain. It probably would look something like this:
- Lol what? You can’t just have
template<SPACE>
in the middle of the code. That’s invalid code! - Oh..it builds with GCC? I guess GCC has some kind of bug?
- Wait, what? That’s valid code?
- Okay, well it’s valid in general but is it valid here? Do we fit all the requirements?
This journey is already giving me too much credit. I doubt I get to the last part but even if I do, then what? How do I find out that Clang is intentionally not matching GCCs behavior because they believe it to be the incorrect interpretation of the standard?
Conclusion
I don’t know man, why are we writing C++?
Scott Meyers, author of Effective C++, describes C++ as an amalgamation of multiple different languages bolted together. He puts it more politely as “multiparadigm” and accurately points out that this results in the language having a lot of confusing patterns as the different paradigms of the language fight each other. Scott Meyers made that description in 1998–since then C++ has added even more features!
I am hoping there can be a cleaner successor language that lets you easily use C++ code when you need to but also allows you to ignore the C++ complexity when you aren’t touching the C++ code directly. Maybe that’s cppfront transpiler or maybe it’s carbon or maybe that other one. But I hope it’s something.
Previously, this paragraph also said “Currently, Clang is proposing a change to the C++ standard to explicitly disallow the use of the disambiguation when it isn’t needed.”. But it has been pointed out to me that this proposal is irrelevant. Clang did get some language tightened up for C++11 but there is no open proposal here. ↩︎