Appendix B: Migration Guides
These guides offer practical paths for upgrading old codebases. Each section focuses on specific transitions that teams commonly face. The aim is to preserve behavior while adopting safer and clearer constructs. Treat each guide as a menu rather than a mandate; choose steps that match your project’s constraints and timelines.
From C to C++ safely
C and C++ share many primitives, yet they model resources and errors in very different ways. A careful migration keeps the low level strengths of C code while lifting it into safer abstractions. The steps below help you avoid brittle rewrites and keep risks contained.
Start with compatible subsets
Begin by compiling your C code as C++ with only minimal changes. Fix the obvious errors first: implicit int, incompatible pointer casts, mixed declarations and statements, and function pointer mismatches. Resolve global name collisions by introducing namespaces and wrapping headers with extern "C" where needed.
// c_compat.hpp
extern "C" {
#include "legacy_api.h"
}
Adopt RAII for resources
Translate malloc and free pairs into smart pointers or resource wrappers. Use std::unique_ptr with custom deleters for objects that must follow C allocation rules. Replace manual cleanup paths with RAII so errors cannot skip cleanup logic.
using CHandle = std::unique_ptr<CObj, void(*)(CObj*)>;
CHandle make_handle() {
return CHandle(create_cobj(), destroy_cobj);
}
Encapsulate unsafe interfaces
Wrap raw C APIs in thin C++ layers that hide pointer arithmetic and manual lifetime management. Turn arrays plus lengths into std::span; turn error codes into std::expected. Keep wrappers minimal so that behavior remains close to the original C API.
Introduce value types
Gradually replace ad hoc structs with small classes that enforce invariants. Provide constructors that establish valid states and give each type a clear ownership model. Start with uncontroversial types such as configuration objects and simple data carriers.
From C++11/14 to C++20 and beyond
Moving from older C++ standards to C++20 and later brings major productivity gains. You gain modules, concepts, ranges, constexpr expansion, and clearer concurrency tools. The transition works best when done in waves rather than a single large jump.
Audit build features
Enable the newer standard in your build system and fix any deprecated features that surface. Update third party libraries that assume older dialects. Check that your compilers and CI runners support the required flags and module scanning.
# CMakeLists.txt …
set(CMAKE_CXX_STANDARD 23)
Replace SFINAE with concepts
Concepts provide readable constraints that remove the need for verbose SFINAE expressions. Retire templates that rely on enable_if pyramids or tag dispatch when concepts express intent more clearly.
// old
template<typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
T twice(T x){ return x + x; }
// new
template<std::integral T>
T twice(T x){ return x + x; }
Adopt ranges
Ranges replace iterator boilerplate and encourage pipeline style transformations. Convert simple loops first. Keep conversions small so that behavioral differences are easy to track. Use views and lazy evaluation to avoid needless allocations.
Strengthen constexpr usage
C++20 and later allow more functions and data structures to become constexpr. Identify pure computations that benefit from compile time evaluation and mark them accordingly. This clarifies intent and can improve performance in hot paths.
Replacing legacy idioms with modern tools
Modern C++ offers libraries and idioms that improve safety and clarity compared with older patterns. The following swaps help reduce technical debt without altering high level behavior.
Raw pointers to smart pointers
Replace owning raw pointers with std::unique_ptr or std::shared_ptr. Retain raw pointers only for observed non owning access. This clearly separates lifetimes and prevents accidental deletes.
// legacy
foo* make();
// modern
std::unique_ptr<foo> make();
Manual arrays to std::vector and std::array
Use std::vector for dynamic sequences and std::array for fixed size data. These containers manage lifetime, bounds, and exception safety. Pair them with std::span when passing views into algorithms.
Loops to algorithms and ranges
Replace simple loops with std::ranges:: algorithms. This removes bookkeeping, reduces off by one errors, and makes intent clearer. Keep complex loops intact where readability would suffer from a forced algorithm.
Macros to inline functions and constants
Macros hide types and scopes. Whenever possible, replace them with constexpr variables or inline functions so that the compiler can check types and lifetimes. This improves debugging because breakpoints behave predictably.
Common portability traps
Portability issues arise when code assumes behavior that varies across compilers, platforms, or libraries. Identifying these assumptions early saves time when targeting new systems. The table and notes below highlight frequent trouble spots.
Undefined and implementation defined behavior
Certain constructs behave differently across platforms. Keep these areas in mind when auditing older code.
| Area | Risk |
| Signed overflow | Undefined; may wrap, trap, or optimize away |
| Object aliasing | Strict aliasing rules may break pointer casts |
| Order of evaluation | Some nested expressions evaluate operands in different orders |
| Char signedness | Platform dependent; affects indexing and comparisons |
| Thread scheduling | Timing assumptions differ across OSes |
Filesystem and path quirks
Paths vary in separators, case sensitivity, and length limits. Use std::filesystem::path to abstract details where possible. Normalize user input and avoid hardcoded separators in low level code.
Endianness and data formats
Binary protocols and file formats often specify byte order. Convert with explicit functions and avoid writing plain structs to disk unless you control the entire ecosystem. Check for padded fields and alignment differences when exchanging data across languages.