Controlling the C++ Runtime Linkage: A Portable Solution

A large number of open-source C++ projects choose Modern CMake as a powerful tool used for managing the building process across multiple platforms. Modern CMake refers to the practices, features, and methodologies introduced in CMake 3.x (and beyond) that simplify, improve, and modernize the build system configuration for C++ and other languages. It emphasizes clarity, reusability, and portability, and aims to make CMake easier to maintain while leveraging its full potential.

For the most part, the CMAKE_<LANG>_FLAGS CMake variable is provided for consumers to set additional compiler flags for the <LANG> programming language. For example, CMAKE_CXX_FLAGS impacts the current C++ compiler and CMAKE_C_FLAGS will take effect when building a C target in the subsequent tasks.

if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
  string(APPEND CMAKE_CXX_FLAGS " /EHsc")
else()
  string(APPEND CMAKE_CXX_FLAGS " -fexceptions")
endif()

The CMAKE_<LANG>_FLAGS variable will always override both Release and Debug targets. To set flags separately for one single build type, extra variables like CMAKE_<LANG>_FLAGS_RELEASE and CMAKE_<LANG>_FLAGS_DEBUG are introduced for such kind of usage.

if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
  string(APPEND CMAKE_CXX_FLAGS_DEBUG " /Od")
  string(APPEND CMAKE_CXX_FLAGS_RELEASE " /O2")
else()
  string(APPEND CMAKE_CXX_FLAGS_DEBUG " -O0")
  string(APPEND CMAKE_CXX_FLAGS_RELEASE " -O3")
endif()

On Windows, Visual Studio 2015 and later versions of Visual Studio all use one Universal Visual C Runtime Library, called the Universal CRT (UCRT). The UCRT is a Microsoft Windows operating system component. It’s included as part of the operating system in Windows 10 or later, and Windows Server 2016 or later. The Visual C++ Runtime Library is usually distributed with the corresponding MSVC tooling or installed independently with third-party software. The VC++ Runtime Library depends on the UCRT in the operating system.

The UCRT and the VC++ Runtime have static and dynamic libraries within MSVC and thereby the MSVC’s compiler cl.exe provides several options for linkage mode control of the two runtimes simultaneously. The options are:

Option (Release)Option (Debug)Description
/MD/MDdLinking to UCRT & VC++ Runtime statically
/MT/MTdLinking to UCRT & VC++ Runtime dynamically

For GCC on Linux or Unix platform, this compiler is designed to use options, such as -static-libstdc++ and -static-libgcc, to enable the static libraries of libstdc++.a, libgcc.a, or libc++.a. The libgcc is known as the GCC Low-level Runtime Library, which exists on some platform and GCC generates calls to routines in this library automatically, whenever it needs to perform some operation that is too complicated to emit inline code for.

For Clang on these platforms, it contains the same options as GCC has for compatibility considerations. The options in Clang are aliases to -static-libc++ and -static-compiler-rt, which are identical to those of GCC.

CompilerOptionDescription
GCC-static-libstdc++Linking to libstdc++ statically
GCC-static-libgccLinking to libgcc statically
Clang-static-libc++
-static-libstdc++
Linking to libc++ statically
Clang-static-libgcc
-static-compiler-rt
Linking to libcompiler-rt statically.

In a C++ project, it is recommended to keep the same configuration of compilation for all dependencies. Because of different compilers and option names, it is a bit complex to write CMake code in a portable way and users may set the options via -DCMAKE_<LANG>_FLAGS=xxx in the command line, that could lead to incorrect building configurations or an unexpected behavior.

A solution is to define a CMake option in convention, e.g. WITH_STATIC_RUNTIME, indicating that it is the official method to modify the runtime linkage mode.

option(WITH_STATIC_RUNTIME OFF "Linking to the C++ runtime statically.")

A user-defined CMake function is constructed here to generate flags for different compilers correspondingly, complying the aforementioned rules.

function(es_make_c_cxx_runtime_flags)
  set(options STATIC_RUNTIME)
  set(one_value_args RESULT_DEBUG_FLAGS RESULT_RELEASE_FLAGS)
  set(multi_value_args "")
  cmake_parse_arguments(PARSE_ARGV 0 ARG "${options}" "${one_value_args}" "${multi_value_args}")
  es_ensure_parameters(es_make_c_cxx_runtime_flags ARG RESULT_DEBUG_FLAGS RESULT_RELEASE_FLAGS)

  if(CMAKE_C_COMPILER_ID STREQUAL "MSVC" OR CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
    if(ARG_STATIC_RUNTIME)
      set(debug_flag /MTd)
      set(release_flag /MT)
    else()
      set(debug_flag /MDd)
      set(release_flag /MD)
    endif()
  elseif(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
    if(ARG_STATIC_RUNTIME)
      set(debug_flag "-static-libstdc++ -static-libgcc")
      set(release_flag ${debug_flag})
    else()
      set(debug_flag "")
      set(release_flag "")
    endif()
  else()
    message(FATAL_ERROR "Unsupported compiler: ${CMAKE_CXX_COMPILER_ID}.")
  endif()
  
  set(${ARG_RESULT_DEBUG_FLAGS} ${debug_flag} PARENT_SCOPE)
  set(${ARG_RESULT_RELEASE_FLAGS} ${release_flag} PARENT_SCOPE)
endfunction()

The es_ensure_parameters function raises a fatal error when the mandatory parameters do not exist. Here is an example showing how to use this function:

if(WITH_STATIC_RUNTIME)
  set(runtime_switch STATIC_RUNTIME)
else()
  set(runtime_switch "")
endif()

es_make_c_cxx_runtime_flags(
  ${runtime_switch}
  debug_flags
  release_flags
)

message(STATUS "debug_flags: ${debug_flags}")
message(STATUS "release_flags: ${release_flags}")

Another design goal is exception safety, that is whatever users set the flags to never corrupts the building process. A workaround is to clear the old runtime flags first and set the new flags after that. A possible implementation is to iterate all the variables to remove conflicting flags and then append the generated flags to these variables. The string(REPLACE ...) function is capable of text replacement operations.

string(REPLACE "string-to-find" "replacement" <RESULT> "source")

set(input_str "Hello world")
string(REPLACE "Hello" "Echo" output_str "${input_str}")
message(STATUS "${output_str}")

In CMake, a list is actually strings separated by semicolons, so an ordinary string can be transformed to a list after inserting semicolons within it. The CMAKE_<LANG>_FLAGS and CMAKE_<LANG>_<BUILD_TYPE> variables use spaces as delimiters and can be passed to the foreach statement by replacing all spaces with semicolons. It is viable to retrieve conflicting flags by reversing the WITH_STATIC_RUNTIME option.

if(ARG_STATIC_RUNTIME)
  set(runtime_switch STATIC_RUNTIME)
  set(reverse_runtime_switch "")
else()
  set(runtime_switch "")
  set(reverse_runtime_switch STATIC_RUNTIME)
endif()

es_make_c_cxx_runtime_flags(
  ${reverse_runtime_switch}
  RESULT_DEBUG_FLAGS reverse_debug_flags
  RESULT_RELEASE_FLAGS reverse_release_flags
)

string(REPLACE " " ";" reverse_debug_flag_list "${reverse_debug_flags}")
string(REPLACE " " ";" reverse_release_flag_list "${reverse_release_flags}")

foreach(item IN LISTS reverse_debug_flag_list)
  string(REPLACE ${item} "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
  string(REPLACE ${item} "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
  string(REPLACE ${item} "" CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}")
  string(REPLACE ${item} "" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}")
endforeach()

foreach(item IN LISTS reverse_release_flag_list)
  string(REPLACE ${item} "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
  string(REPLACE ${item} "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
  string(REPLACE ${item} "" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")
  string(REPLACE ${item} "" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
endforeach()

Now all the variables do not contain conflicting flags anymore and the new flags can be appended to them immediately.

es_make_c_cxx_runtime_flags(
  ${runtime_switch}
  RESULT_DEBUG_FLAGS debug_flags
  RESULT_RELEASE_FLAGS release_flags
)

string(APPEND CMAKE_C_FLAGS_DEBUG " ${debug_flags}")
string(APPEND CMAKE_CXX_FLAGS_DEBUG " ${debug_flags}")
string(APPEND CMAKE_C_FLAGS_RELEASE " ${release_flags}")
string(APPEND CMAKE_CXX_FLAGS_RELEASE " ${release_flags}")

If all the above code are located inside a CMake function, which has its own inner scope of variables. Extra set statements are necessary to copy the inside variables to the outer ones.

set(CMAKE_C_FLAGS ${CMAKE_C_FLAGS} PARENT_SCOPE)
set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} PARENT_SCOPE)
set(CMAKE_C_FLAGS_DEBUG ${CMAKE_C_FLAGS_DEBUG} PARENT_SCOPE)
set(CMAKE_CXX_FLAGS_DEBUG ${CMAKE_CXX_FLAGS_DEBUG} PARENT_SCOPE)
set(CMAKE_C_FLAGS_RELEASE ${CMAKE_C_FLAGS_RELEASE} PARENT_SCOPE)
set(CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE} PARENT_SCOPE)

Well done! Everything might be OK now. The full application can be checked at my personal repo named “Cpp Essence” on GitHub. I will appreciate your stars and contributions there! Big thanks.