How to Modernize Legacy C++ Code?

Amir Kirsh
Amir Kirsh / 8月 23 2021
How to Modernize Legacy C++ Code?

C++ has evolved over the past decade – so much so, that some see it as a whole different language altogether, compared to the “old legacy C++”. Well, even though it is the same language with the same principles and basic syntax, the additions to the language and to the standard library are significant. 

But it seems not everyone is taking advantage of the newest versions. True, according to Jetbrains 2021 EcoSystem Survey, most projects and companies have already made the big move and progressed to the newer specifications of the language. However, while we already have C++20 out there, there are still 12% of developers in the survey that are based on C++98/03 and 40% who are ‘stuck’ on C++11. The majority, that is 42%, is on C++17.
Jetbrains 2021 EcoSystem Survey_C++ standards
Source: Jetbrains 2021 EcoSystem Survey

The survey clearly shows that in most cases developers and companies are not eager to migrate, and where migration is considered, it is not to the latest standard (C++20), but rather to C++14 or C++17. The current adopters of C++17 are a bit of an exception in that sense, as they are quite eager to migrate to C++20.

Jetbrains 2021 EcoSystem Survey_Andreas Kling quote
Source: Jetbrains 2021 EcoSystem Survey

So Why Would You Go For a Newer Version Anyway?

There are some very good reasons to progress to modern C++ versions:

  • Get more possibilities via newer syntax and library options, which means potentially better code and enhanced performance
  • Use of standard libraries and features that become part of the standard library, instead of using external libraries, making your code easier to maintain and easier for developers to transition to.
  • Let’s face it, developers like to progress. You don’t want your team to be left behind, or else they would look for other projects.
  • Staying behind with the language version does not necessarily mean staying behind with OS and compiler versions, but it usually does. That, in turn, holds additional risks, from compatibility to security issues.

Why Do Companies Stay Behind?

Stay behind_legacy code

The main reason for companies to still use older versions (yes, even C++98/03 or C++11), is compiler support. In case your product depends on a very specific OS (even if for part of your installations) which does not have a modern C++ compiler, you have a problem.

Another case is where the effort in upgrading the codebase seems not to be worth the hassle, as the product is a legacy product. There are cases where the company doesn’t have the manpower to support such an upgrade. And in some cases, the upgrade seems risky.

For any of the reasons mentioned above (or others), staying behind is not a good option. You gradually build your technical debt and your code becomes obsolete. Just think, when you’re about to develop a new feature, or fix a bug, you would be ‘stuck’ with the code allowed by the old legacy C++ version you’re working with, which might be quite narrowing. It would prevent you from using many of the modern C++ code examples out there, as well as many libraries and utilities that rely on modern C++ syntax.

Now that we understand the importance of transitioning to modern C++, let’s discuss the path for migration.

Migration Strategies – Moving From Legacy C++ to Modern C++, Keeping Your Health and Sanity Intact

The first obstacle might be your operating system. Older OS versions may have C++ compilers that don’t support modern C++. For example, older versions of VxWorks had support only for older versions of the WindRiver Diab compiler which supported C++03 but not C++11. In the current VxWorks version, the Diab compiler is based on LLVM and supports modern C++ versions. Additionally, newer versions of VxWorks also have GNU and ICC compiler support. But in order to use any of these, if you are still on old VxWorks versions, there is a need to migrate first to a newer version of the underlying OS. This is a project of its own. And that’s just one example. Similar OS obstacles arise with other legacy environments as well.

In some cases, you are required to support the old OS version, as this requirement originates from customers, or because you are using old hardware. Managing customer requirements and announcing End of Life for your software products is not part of this article, but I would hint that this is a necessary step.

In case you still have to support old OS and old compilers along with your progress to new environments, you can do that by isolating the shared parts of the code that are used by all environments, and keeping them as-is, while migrating the parts related to the newer environments.

If you are switching an OS or compiler, your first step would be to build your code, as-is, on the newer OS (if required) and with the newer compiler, without changing the C++ version. The mere change of the compiler may raise issues that you would have to fix, before trying to compile with a more advanced C++ version. Such issues are usually due to different strictness levels of different compilers, in different aspects. And as one compiler might be more liberal in a certain aspect, another one might be more strict, which would result in errors that you will have to fix. You could probably overcome such errors by playing with your error flags (removing, for example, -pedantic or -Wall), while adding a note in your project that these error flags shall be added back later on. 

A side note: if your project is working with static or dynamic libraries, either your own or external, and if these libraries are still compiled with an old version of your compiler (all the more so with another compiler), compatibility is not assured. The ABI (Application Binary Interface) may break, which may require you to recompile dependent libraries with your newer compiler.

The next step (which is the first one if you are staying with the same OS and compiler and just advancing with your C++ version) is to set the proper flags, to make your build use the more advanced C++ version that you aim for (e.g. -std=C++17). Since the C++ versions are mostly backward compatible (with very few cases of bug fixes in the language itself, which require changes that are not backward compatible), the code may just compile as-is. However, it might be that you would get compilation errors if, again, the compiler is more strict about things that were not considered an error before but do now. And again, you may try to overcome such errors by removing some error flags, usually as part of your migration project management, allowing you to postpone the required fixes for a later stage. 

Once your project compiles and runs with the new C++ version, you can celebrate – but not too long. It now comes to full certification – making sure all works the same. There are many reasons why rebuilding code in another environment may change its behavior, and all are based on defects that were hiding in your code and are now popping up. It may relate to code that relies on undefined behavior and thus does not ensure any specific behavior, or code that relies on unspecified behavior and thus ensures a specific behavior that is picked by the compiler but may change when you change your compiler. And if your application is multithreaded or based on timings, changes in timing are quite common after rebuilding, with any minor change in the environment, thus potentially raising new race conditions and data races that were hiding in your code before.

Passing full certification brings you to the real starting point: your code compiles, runs and works as expected after being compiled with newer, more modern C++ version.

You may already have benefits. For example, if you upgraded from C++98/03 to C++11 or higher, usage of standard library containers may be more efficient based on Rvalue and move semantics, even without any change in your code! But in order to fully utilize the change, you want to start using advanced C++ features. And the question is where to start.

Small Incremental Changes, With a Reason

If it works, and there is no reason for a change – don’t touch it.

  • Hold your horses before replacing all loops to range-based-for.
  • Do not rush to auto all your variable declarations.
  • Wait before you replace an existing loop with a new algorithm that was added to the standard library.
  • Don’t feel obliged to move all your old allocations to smart pointers.

My recommendation is to find the places in code that would earn most from a change, then touch all the rest with great caution, when you add new features or fix defects. For example, if your upgrade is from C++98/03 to any newer version of C++, code that may benefit from changes and should be searched for may include:

  • Classes that are not under the rule-of-zero (i.e. they have either user defined copy constructor, assignment operator or destructor) and may enjoy performance gains having move constructor and move assignment operator.
  • Places where you move around data structures without the use of std::move (of course, the addition of std::move should be done with great care, and only where relevant).
  • Places where you pass objects to data structures and may emplace them into the data structure.
  • Any usage of an external library that could easily be replaced with the standard library, making your code less dependent on external libraries.

In any case, it goes without saying that any change requires code review. Even in cases where the developer feels that nothing major is being done, any change bears its risks and requires a review.

The gradual move to utilizing the new features of modern C++ will keep your code in a state that may look odd, for a long period of time: there are parts that are modernized and parts that look quite old, sometimes in the exact same file. That’s OK, Rome was also not built in a day.

Summary

Migrating to modern C++ versions is basically a must if you have a C++ project that is live and kicking. Staying behind exposes you to technical debt, not being able to use newer C++ syntax, as well as external code and libraries that require the use of the more modern versions. You will also find it hard to keep your developers, as developers tend to seek out projects that use modern syntax and tools.

We discussed the strategies for tackling a migration project. You should remember that it is an actual project, not a side job, not something that you just do in one afternoon. It requires thorough planning and clear certification. However, when done right, you may find that your fears were mostly misguided.

One thing to remember is that just migrating to a newer language version is not enough. If the team doesn’t know how to properly use modern C++ features and abilities, you may find new bugs related to misuse of the language, or just small or big inefficiencies related to not being aware of the nitty gritty details.

During a migration project, there are going to be a lot of builds. Yes, you are going to compile a lot, and in many cases to fix something and recompile. All these builds and compilations require time, which may significantly delay you. This is where Incredibuild comes to the rescue! With Incredibuild’s build acceleration, you can finish such a migration faster and much more sanely. This is of course not the only reason for using Incredibuild’s build acceleration, but it is certainly a good use case to start with, if you are not using it already.

Final note: if you decide on migrating to modern C++, try being brave. Jump directly to C++20; even if your compiler has only a partial support for C++20 it is still worth it. Any new C++ version brings with it many goodies, and C++20 is not different – with significant features (“the big four”) being: Concepts, Modules, Ranges and Coroutines. 

speed up c++

Stay informed!

Subscribe to receive our incredibly exclusive content

Amir Kirsh

Amir Kirsh, Incredibuild's Dev Advocate, is a C++ lecturer at the Academic College of Tel-Aviv-Yaffo and at Tel-Aviv University, previously the Chief Programmer at Comverse, after being CTO and VP R&D at a startup acquired by Comverse. He is also a co-organizer of the annual Core C++ conference and a member of the ISO C++ Israeli National Body.