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 a constexpr function that calculates the factorial of a number:

#include <stdio.h>
#include <stdlib.h>
constexpr int factorial( int x )
{
    if ( x < 1 )
        return 1;
    else
        return x*factorial( x - 1 );
}

int main( int argc, char*argv[] )
{
    if ( argc < 2 )
        return 1;
    const int x = atoi(argv[1]);
    printf( "factorial(%x) = %i\n", x, factorial( x ) );
    return 0;
}

Since a variable x that can only be known at runtime is passed to factorial, the function has to be evaluated at runtime.

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 need to be/are marked covered, even if they are not used at runtime.
  • --cs-constexpr=runtime: only functions that are executed at runtime are tracked for coverage analysis.

Let us first look at the last option, the instrumentation at runtime only. In this mode, when a function is executed at least once, Coco will require that all its code paths are executed to consider it as covered.

In the following screenshots, we see CoverageBrowser with two executions of the factorial program loaded. The first is run without arguments and therefore does not cover the factorial() function; the second one (which is here disabled) computes factorial(0).

As we can see here, the factorial function is marked as needing coverage – exactly because it was not executed during this test.

In the next screenshot, both executions are enabled and we see that the execution of factorial() is partially covered, and one line of it still needs to be covered by an additional test.

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 this code is used. For this reason, we have added the possibility to record the coverage of all constexpr functions by using the switch --cs-constexpr=full. The drawback is the need to write dedicated unit tests that force the usage of the run-time version. This is straightforward to do: in most cases, one only needs to pass non-constant variables as input to a constexpr function.

Consider the following test as an example:

void test_fact0()
{
    CHECK( factorial(0) == 1 );
}

Since the argument to the factorial function is a constant expression, the factorial function will be evaluated at compile-time.

To make sure that the test is evaluated at runtime, and covers the factorial function, we only need to use a non-const variable for the input of the factorial function:

void test_fact0()
{
    int n = 0;
    CHECK( factorial(n) == 1 );
}

Instrumenting constexpt function requires a minimal C++ standard. Here is the list:

constexpr supportCoverage MethodeC++ 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.1.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.