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: all constexpr 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 unused constexpr 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 supportCoverage MethodC++ Standard Required
--cs-constexpr=ignoreallall
--cs-constexpr=fullStatement block, decision and conditionC++17
--cs-constexpr=fullMCC and MC/DCC++20
--cs-constexpr=runtimeallC++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.