There is no such thing as portable C++ code

...there is only C++ code that compiles on all the compilers and platforms you tested.

For about 4 years now, my full-time job involves work on a C++ codebase that has to compile for about half a dozen operating systems and on about as many different compilers. This cross-platform work is always incredibly tedious, even if I take great care to write clean cross-platform C++ code. I keep breaking the build if I don't run all the compilers for all the target platforms. Are the mistakes of my own making? Sure, but consider this:

#include <vector> int main(int argc, char* argv[]) { return std::max(0,1); }

This snippet compiles in every GCC, but it fails to compile in every Visual Studio I've tried except for msvc v19.27. Clearly, this small program is not portable. What is the mistake?

<source>(4): error C2039: 'max': is not a member of 'std'

We included <vector>, but we should have included <algorithm>, which is the header that defines std::max. I will spare you my opinion on being forced to include a large and complex header like <algorithm> for the ubiquitous one-liners std::max and std::min. Also, it is unclear why exactly <vector> is allowed to include std::max on GCC. Anyway, so it's just a programmer error, we can fix the mistake and it compiles. Sure, but imagine that this file has 1000 lines of code and includes 20 other files with just as many lines and includes. At some point, you've completely lost the overview: Which includes are already present? What do I still need to include? Even if I could have the perfect overview (I cannot), most C++ work involves existing codebases that are very large, think hundreds of thousands of lines of code or even millions, and these codebases already contain many such include problems which usually rear their ugly head as soon as the code is touched.

The obvious consequence is that even if you write your code carefully and compile it on your platform, there is a significant risk that you will break the build on one of the other platforms. You would have to compile your code on every compiler for every platform to be sure you didn't break anything. I get flashbacks to web development in the early 2000's, where every line of HTML had to be tested on every browser in existence, and you could be certain that every single browser did a different thing. The unspecified behaviour during C++ compilation is a direct consequence of the fact that C++ includes are mere macros that paste code into your file and the fact that the C++ standard does not accurately specify what enters your source code when you include an STL header. These problems aren't even subtleties, they are fundamental, structural problems of an ageing language.

There are other such problems that ruin code portability. An example that happened recently was #include "foo\bar.h". Given a file bar.h in the subdirectory foo, this compiles perfectly on Windows. The programmer did not recognize that he used the Windows path delimiter (\) instead of the standard Linux one (/). Half our build servers run on Windows, half of them run on Linux. The error was only found when the build on one of the Linux build servers failed. The same thing goes for case sensitivity: #include "foo/Bar.h" compiles on Windows, but fails on Linux if the (lowercase) file bar.h is present. These things are specification issues at the core of the preprocessor. They are oversights that make it harder to write portable code.

A similar issue in C++ is the confusion of #include <stdlib.h> vs #include <cstdlib>. Take for instance the following code:

#include <stdlib.h> int main(int argc, char* argv[]) { std::malloc(1); return 0; }

This code compiles on Visual Studio, but does not compile on older GCCs (version < 6):

<source>:4:5: error: 'malloc' is not a member of 'std'

The reason why it doesn't compile on older GCCs is that when you include the C-style header (stdlib.h), it does not put the included functions into the std:: namespace, it only does that when you include the C++-style header (cstdlib). However, some C++ compilers also put the included functions into the std:: namespace if you include the C-style header, and that is why those compilers are able to compile this program. Conversely, if you include the C++-style header, some compilers only provide the functions in the std:: namespace, making the following program fail to compile:

#include <cstdlib> int main(int argc, char* argv[]) { malloc(1); return 0; }

(Note: In C++, always use the C++ style include, e.g. #include <cstdlib>. This ensures proper population of the std:: namespace. Always use the functions inside the std:: namespace. You may still run into build problems, but the likelihood is decreased.)

These are just a few of many, many build problems a programmer faces when writing cross-platform C++ code. Other grievances include:

  • The order in which header files are included matters and can lead to compilation or even runtime errors
  • Large number of incompatible build systems
  • Visual Studio quirks like #include "stdafx.h" infest the codebase

Long story short: Whether your cross-platform code compiles might as well be roulette and the stakes are high: Your sanity and a bunch of time and money.

At this point, I spend more time maintaining build systems and making sure everything compiles on all the compilers for all the operating systems than I spend writing and debugging code. I spend precious time and energy on things that could be automated. In other words, the tooling shows its age, it is failing me.

I looked for alternatives to C++ and found Rust which avoids almost all these pitfalls with its superior build system. With its installation, you get a tool called cargo which downloads dependencies and manages your build. No longer will you fight cmake, automake, make, shell scripts, python scripts, pearl scripts, Visual Studio projects or other build solutions for each one of your dependencies. Compiling an entire Rust application or library, including all its dependencies, is as simple as git clone https://github.com/rust-lang/log && cd log && cargo build. Now, I know, C++20 introduced modules which will alleviate some of these problems. But I will have to wait 15-25 years until a compiler for such a modern standard is available on all our embedded platforms. And starting to use modules would mean rewriting or at least refactoring large parts of the code. The temptation is big: Why not just gradually phase out C++ and start using a thoroughly modern alternative like Rust instead?

Post comment

CAPTCHA
* required field

Comments

(no comments yet)