Recently, Herb Sutter wrote an excellent article on C++ safety. He discussed numerous ideas, but I’ll provide a summary of his perspective on what can be done in the medium term to enhance C++ safety.
In C++, by default enforce … | (A) Solution for new/updated code (can require code changes — no link/binary changes) | (B) Solution for existing code (requires recompile only — no manual code changes, no link/binary changes) |
Type safety | Ban all inherently unsafe casts and conversions | Make unsafe casts and conversions with a safe alternative do the safe thing |
Bounds safety | Ban pointer arithmetic Ban unchecked iterator arithmetic | Check in-bounds for all allowed iterator arithmetic Check in-bounds for all subscript operations |
Initialization safety | Require all variables to be initialized (either at declaration, or before first use) | — |
Lifetime safety | Statically diagnose many common pointer/iterator lifetime error cases | Check not-null for all pointer dereferences |
Less undefined behavior | Statically diagnose known UB/bug cases, to error on actual bugs in existing code with just a recompile and zero false positives: Ban mathematically invalid comparison chains (add additional cases from UB Annex review) | Automatically fix known UB/bug cases, to make current bugs in existing code be actually correct with just a recompile and zero false positives: Define mathematically valid comparison chains Default return *this; for C assignment operators that return C& (add additional cases from UB Annex review) |
But what are the current possibilities for achieving this goal?
Type Safety:
To enforce type safety in C++, you can use the following compilation command with various flags to enable strict type checking and other safety features:
g++ -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wshadow -Werror -o output_file source_file.cpp
Here’s a breakdown of what these flags do:
-Wall
: Enables all the common warnings.-Wextra
: Enables additional warnings.-Wpedantic
: Enforces strict ISO C++ compliance.-Wconversion
: Warns about implicit conversions that may change a value.-Wsign-conversion
: Warns about implicit conversions between signed and unsigned types.-Wshadow
: Warns if a variable declaration shadows one from an outer scope.-Werror
: Treats all warnings as errors, stopping compilation if any warnings are present.
Bounds Safety:
To enforce bounds safety in C++, you can use the -fsanitize=bounds
option with gcc
or clang
. This flag enables runtime checks for out-of-bounds array access. Here’s a sample compilation command:
g++ -fsanitize=bounds -o my_program my_program.cpp
This command will compile my_program.cpp
with bounds safety checks enabled and produce an executable named my_program
. If you encounter any out-of-bounds access during execution, the sanitizer will report it.
Initialisation Safety:
To enforce initialization safety in C++, you can use several compiler flags that help catch uninitialized variables and other related issues. Here are the commands for GCC and Clang:
GCC
g++ -Wall -Wextra -Wuninitialized -Wmaybe-uninitialized -o your_program your_program.cpp
Clang
clang++ -Wall -Wextra -Wuninitialized -o your_program your_program.cpp
Explanation
-Wall
: Enables all the commonly used warning messages.-Wextra
: Enables some extra warning flags that are not enabled by-Wall
.-Wuninitialized
: Warns about uninitialized variables.-Wmaybe-uninitialized
: Warns about variables that may be uninitialized.
Life time safety:
To enforce lifetime safety in C++, you need to use specific compiler flags and potentially enable certain features or tools designed to help with lifetime analysis and safety checks. As of recent developments, the -fsanitize=address
flag with the Clang or GCC compiler can help detect lifetime issues. Additionally, you might want to use Clang’s static analyzer or Microsoft’s tools in Visual Studio for more comprehensive checks.
Here are some commands you can use for GCC and Clang:
GCC
g++ -fsanitize=address -fno-omit-frame-pointer -g -o your_program your_program.cpp
Clang
clang++ -fsanitize=address -fno-omit-frame-pointer -g -o your_program your_program.cpp
These commands enable the AddressSanitizer, which helps detect various memory errors including use-after-free, use-after-return, and use-after-scope issues, which are critical for enforcing lifetime safety.
Undefined behavior:
To enforce C++ undefined behavior safety, you can use various compiler flags and tools. Here’s a compilation command using g++
(part of the GNU Compiler Collection) that includes common flags to help detect and prevent undefined behavior:
g++ -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined -fstack-protector-all -O2 -g your_file.cpp -o your_program
Here’s a breakdown of the flags used:
-Wall
: Enables all the commonly used warning messages.-Wextra
: Enables additional warning messages not included by-Wall
.-Werror
: Treats all warnings as errors, forcing you to fix them.-pedantic
: Enforces strict ISO C++ compliance.-fsanitize=address,undefined
: Enables AddressSanitizer and UndefinedBehaviorSanitizer to detect memory errors and undefined behavior at runtime.-fstack-protector-all
: Adds stack protection to detect stack buffer overflows.-O2
: Enables optimization (level 2), which is a good balance between performance and debugging.-g
: Includes debugging information in the binary for use with a debugger (e.g.,gdb
).
C++ sanitizers Issues:
As we can see, sanitizers could address some of the problems reported by Herb Sutter. However, they have several issues:
1- Performance Overhead:
- Runtime Performance: Sanitizers introduce significant runtime overhead. AddressSanitizer, for example, can slow down the execution of a program by 2-3 times.
- Memory Usage: Sanitizers, particularly AddressSanitizer, increase memory usage substantially. This can be problematic for memory-constrained environments.
2- Compatibility Issues:
- Platform Support: Not all sanitizers are available on all platforms or compilers. This can limit their use in cross-platform projects.
- Third-Party Libraries: Using sanitizers with third-party libraries that were not compiled with sanitization in mind can result in compatibility issues or spurious errors.
3- Build and Execution Complexity:
- Complexity in Build Process: Integrating sanitizers into the build process can complicate the build configuration, especially when dealing with multiple build types (e.g., release vs. debug).
- Special Execution Environment: Running tests under sanitizers often requires a special execution environment and additional setup, making the process more cumbersome.
4- Limited Scope:
- Specific Types of Bugs: Sanitizers are designed to catch specific types of bugs (e.g., memory errors, undefined behavior), and may not catch logic errors, algorithmic inefficiencies, or other kinds of bugs.
5- Debugging Complexity:
- Detailed Reports: While detailed reports from sanitizers are helpful, they can sometimes be overwhelming or difficult to interpret, especially for large and complex codebases.
- Reproducibility: Some issues reported by sanitizers might be non-deterministic and difficult to reproduce, complicating the debugging process.
It’s clear that we need another mechanism to enhance safety without compromising C++ performance. This goal has become a high-priority concern for the C++ committee, and we hope to have a definitive solution for this urgent problem soon.