The vulnerability as described in ISO/IEC TR 24772-1:2019 clause 6.51 applies to C++.
The C++ pre-processor allows the use of macros that are text-replaced before compilation.
Function-like macros look similar to functions but have different semantics. Because the arguments are text-replaced, expressions passed to a function-like macro may be evaluated multiple times. This can result in unintended and undefined behaviour [EWF] if the arguments have side effects or are pre-processor directives. Additionally, the arguments and body of function-like macros should be fully parenthesized to avoid unintended and undefined behaviour.
The following code example demonstrates undefined behaviour when a function-like macro is called with arguments that have side-effects (in this case, the increment operator) .
#define CUBE(X) ((X) * (X) * (X))
// ...
int i = 2;
int a = 81 / CUBE(++i);The above example could expand to:
int a = 81 / ((++i) * (++i) * (++i));which has undefined behaviour so this macro expansion is difficult to predict.
Another mechanism of failure can occur when the arguments within the body of a function-like macro are not fully parenthesized. The following example shows the CUBE macro without parenthesized arguments.
#define CUBE(X) (X * X * X)
// ...
int a = CUBE(2 + 1);This example expands to:
int a = (2 + 1 * 2 + 1 * 2 + 1)which evaluates to 7 instead of the intended 27.
Both issues shown above are a result of the text replacement mechanisms of preprocessing. Usually such function-like macros can be replaced by type-safe constexpr functions that do not suffer from the vulnerabilities caused by text replacement, as shown below:
constexpr auto CUBE(auto const x) { return x * x * x; }Historically, one use of function-like macros was to provide source-location information via the compiler-provided macros __LINE__, __FILE__, and __func__. Modern C++ provides std::source_location with the std::sources_location::current() default function argument that allows passing that information from a function’s call site without the need to use a macro.
Similar issues can apply to object-like macros:
#define THREE 1+2
auto x = THREE*THREEbecomes
auto x = 1+2*1+2 // 5, not 6 as expectedMost object-like macros can be replaced by constexpr variables that do not suffer from this vulnerability.
A remaining use case for function-like macros is the use of “stringification” (#) of macro arguments or joining macro arguments to form a new token (##). C++ does not specify the order of evaluation, hence care is necessary in macros that contain multiple instances of these operators.
double operator""_ms(const char *, unsigned long );
#define jstringify( x, y ) # x ## y
auto s = jstringify( 0, _ms ); //Expands to "0"_ms or "0_ms"To avoid the vulnerability or mitigate its ill effects, C++ software developers can:
Replace function-like macros with constexpr inline functions where possible, but if a function-like macro must be used, ensure that:
Ensure that arguments to function-like macros do not resemble a preprocessing directive.
Replace object-like macros with constexpr variables where possible.
Replace conditional compilation with the preprocessor with if constexpr where possible, e.g., in function bodies, including cases where compile-time define of a macro (as empty) controls if a macro definition is used to expand to an empty statement or another statement.
Replace preprocessor include directives with module import where possible.
Prefer std::source_location mechanisms over macros that use __LINE__, __FILE__, or __func__.
Only use macros for include guards, to control conditional compilation, or when the macro’s definition requires token pasting (##.{.cpp}) or stringification (#) of macro arguments.
Avoid the use of ## and # in the same macro body.