C++ constexpr handling
The constexpr
specifier declares that a function or variable can be evaluated at compile-time or runtime.
If evaluated at compile-time, the expression itself does not execute any source code and only the result of its compile-time evaluation is used in the executable. A constexpr
function implies inline
so it might be optimized out.
Here is a small example:
constexpr bool use_degree = false; constexpr double right_angle = use_degree ? 90.0 : 1.5708;
Since use_degree
is known at compile-time and the conditional expression for right_angle
uses only this value as a parameter, right_angle
can be completely computed at compile-time. The code is then equivalent to:
constexpr bool use_degree = false; constexpr double right_angle = 1.5708;
The conditional expression was evaluated at compile-time. Since there are no conditions to cover anymore during the execution, it no longer makes sense to cover the possibilities of the expression "use_degree ? 90.0 : 1.5708
".
For constexpr
variables, the situation is simple: They are evaluated once and only at compile-time. The situation is a bit more complicated with constexpr
functions: They may be called many times from different parts of the code, be passed non-const parameters, or possibly be part of a public API. In cases like these, covering their executions may be critical for some applications.
Here is an example with three constexpr
functions that calculate the factorial of a number. The first two are called from main()
, the third is unused.
#include "output.hpp" constexpr int fac(int n) { if (n < 2) return 1; else return n * fac(n-1); } constexpr int fac2(int n) { if (n < 2) return 1; else return n * fac2(n-1); } constexpr int fac3(int n) { return n < 2 ? 1 : n * fac3(n - 1); } int main() { int x = 1; constexpr int y = 4; constexpr int f = fac(y); int g = fac2(x); output_int(f); output_int(g); return 0; }
CoverageScanner
has three different options for handling constexpr
functions:
--cs-constexpr=ignore
:constexpr
functions are simply not taken into account in the code coverage analysis. This is necessary for applications that are built with a language standard earlier than C++17.--cs-constexpr=full
: allconstexpr
functions are considered when measuring code coverage, so unused functions will be marked red.--cs-constexpr=runtime
: only functions that are executed at runtime are tracked for coverage analysis. This means that unusedconstexpr
functions are not counted at all towards the overall coverage, and are not marked red.
Let us first look at the last option, --cs-constexpr=runtime
. Using that option on the example above, we should see MC/DC coverage like the image below.
fac
and fac2
are both invoked from main
, but the evaluation of fac
can be done at compile-time, while the call to fac2
forces a runtime execution of the function, which can be measured by Coco. With --cs-constexpr=runtime
, fac
and fac3
are both ignored in Coco's coverage measurements.
CoverageScanner
discovers what needs to be covered when tests get executed. By this mechanism, it ignores the constexpr
functions which have no generated code. This is a good behavior for application testing but can be problematic for API testing.
The problem with API testing is that the developer does not know whether the compiler decides to compute the result of a function at compile-time or at run-time in the target – it depends on the way the code is used. For this reason, we have added the possibility to measure the coverage of all constexpr
functions by using the switch --cs-constexpr=full
. Then, to get full coverage, one must write dedicated unit tests that force runtime invocations of them. This is straightforward to do: in most cases, one only needs to pass non-constant variables as input to the constexpr
functions.
With --cs-constexpr=full
, shown above, fac
and fac3
are now marked red, as needing coverage.
In both screenshots, we see that the execution of fac2()
is partially covered, and one line of it still needs to be covered by an additional test.
Consider the following test as an example:
void test_fact0() { CHECK( fac(4) == 24 ); CHECK( fac2(5) == 125 ); }
Since the arguments to these functions are constant expressions, these constexpr
invocations will be evaluated at compile-time. Coco will not be able to measure coverage of them, and therefore test_fact0
won't contribute to the overall coverage of our tests.
To make sure that the constexpr
functions are evaluated at runtime, we need to pass non-const variables to them.
void test_fact1() { int n = 4; CHECK( fac(n) == 24 ); CHECK( fac2(n) == 24 ); CHECK( fac3(n) == 24 ); }
Running test_fact1()
should result in full coverage of all three functions.
Instrumenting constexpr
functions requires a minimal C++ standard. Here is the list:
constexpr support | Coverage Method | C++ Standard Required |
---|---|---|
--cs-constexpr=ignore | all | all |
--cs-constexpr=full | Statement block, decision and condition | C++17 |
--cs-constexpr=full | MCC and MC/DC | C++20 |
--cs-constexpr=runtime | all | C++20 |
Coco v7.2.0 ©2024 The Qt Company Ltd.
Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property
of their respective owners.