Use constexpr for faster, smaller, and safer code

With the release of C++14, the standards committee strengthened one of the coolest modern features of C++: constexpr. Now, C++ developers can write constant expressions and force their evaluation at compile-time, rather than at every invocation by users. This results in faster execution, smaller executables and, surprisingly, safer code.

Undefined behavior has been the source of many security bugs, such as Linux kernel privilege escalation (CVE-2009-1897) and myriad poorly implemented integer overflow checks that are removed due to undefined behavior. The C++ standards committee decided that code marked constexpr cannot invoke undefined behavior when designing constexpr. For a comprehensive analysis, read Shafik Yaghmour’s fantastic blog post titled “Exploring Undefined Behavior Using Constexpr.”

I believe constexpr will evolve into a much safer subset of C++. We should embrace it wholeheartedly. To help, I created a libclang-based tool to mark as much code as possible as constexpr, called constexpr-everything. It automatically applies constexpr to conforming functions and variables.

Constexpr when confronted with undefined behavior

Recently in our internal Slack channel, a co-worker was trying to create an exploitable binary where the vulnerability was an uninitialized stack local, but he was fighting the compiler. It refused to generate the vulnerable code.

/* clang -o example example.cpp -O2 -std=gnu++14 \
   -Wall -Wextra -Wshadow -Wconversion
 */
typedef void (*handler)();

void handler1();
void handler2();
void handler3();

handler handler_picker(int choice) {
    handler h;
    switch(choice) {
    case 1:
        h = handler1;
        break;
    case 2:
        h = handler2;
        break;
    case 3:
        h = handler3;
        break;
    }

    return h;
}

When compiling the example code with a modern compiler (clang 8.0), the compiler silently eliminates the vulnerable cases. If the caller specifies a choice not handled by the switch (such as 0 or 4), the function returns handler2. This is true on optimization levels greater than -O0. Try it for yourself on Compiler Explorer!

My default set of warnings (-Wall -Wextra -Wshadow -Wconversion) doesn’t warn about this on clang at all (Try it). It prints a warning on gcc but only with optimizations enabled (-O0 vs -O1)!

Note: If you want to print all the warnings clang knows about, use -Weverything on clang when developing.

The reason for this is, of course, undefined behavior. Since undefined behavior can’t exist, the compiler is free to make assumptions on the code — in this case assuming that handler h can never be uninitialized.

Right now the compiler silently accepts this bad code and just assumes we know what we’re doing. Ideally, it would error out. This is where constexpr saves us.

/* clang -o example example.cpp -O2 -std=gnu++14 \
   -Wall -Wextra -Wshadow -Wconversion
 */
typedef void (*handler)();

void handler1();
void handler2();
void handler3();

constexpr handler handler_picker(int choice)
{
    handler h;
    switch(choice) {
    case 1:
        h = handler1;
        break;
    case 2:
        h = handler2;
        break;
    case 3:
        h = handler3;
        break;
    }

    return h;
}
# https://gcc.godbolt.org/z/gKrZV3
<source>:9:13: error: variables defined in a constexpr 
function must be initialized
    handler h;
            

1 error generated.
Compiler returned: 1

constexpr forced an error here, which is what we want. It works on most forms of undefined behavior but there are still gaps in the compiler implementations.

constexpr everything!

After some digging in the clang source, I realized that I can use the same machinery libclang uses to determine if something can be constexpr during its semantic analysis to automatically mark functions and methods as constexpr. While this won’t detect more undefined behavior directly, it will help us mark as much code as possible as constexpr.

Initially I started writing a clang-tidy pass, but ran into trouble with the available APIs and the context available in the pass. I decided to create my own stand-alone tool: constexpr-everything. It is available on our GitHub and should work with recent libclang versions.

I wrote two visitors, one which tries to identify if a function can be marked as constexpr. This turned out to be fairly straightforward; I iterate over all the clang::FunctionDecls in the current translation unit and ask if they can be evaluated in a constexpr context with clang::Sema::CheckConstexprFunctionDecl, clang::Sema::CheckConstexprFunctionBody, and clang::Sema::CheckConstexprParameterTypes. I skip over functions that are already constexpr or can’t be (like destructors or main). When the analysis detects a function that can be constexpr but isn’t already, it issues a diagnostic and a FixIt:

$ ../../../build/constexpr-everything ../test02.cpp
constexpr-everything/tests/02/test02.cpp:13:9: warning: function can be constexpr
        X(const int& val) : num(val) {
        
        constexpr
constexpr-everything/tests/02/test02.cpp:17:9: warning: function can be constexpr
        X(const X& lVal)
        
        constexpr
constexpr-everything/tests/02/test02.cpp:29:9: warning: function can be constexpr
        int getNum() const { return num; }
        
        constexpr
3 warnings generated.

FixIts can be automatically applied with the -fix command line option.

Trouble applying constexpr variables

We need to mark variables as constexpr in order to force evaluation of constexpr functions. Automatically applying constexpr to functions is easy. Doing so on variables is quite difficult. I had issues with variables that weren’t previously marked const getting marked const implicitly through the addition of constexpr.

After trying to apply constexpr as widely as possible and fighting with my test cases, I switched tactics and went with a much more conservative approach: only mark variables that are already const-qualified and have constexpr initializers or constructors.

$ ../../../build/constexpr-everything ../test02.cpp -fix
constexpr-everything/tests/02/test02.cpp:47:5: warning: variable can be constexpr
const X x3(400);</code>

constexpr
constexpr-everything/tests/02/test02.cpp:47:5: note: FIX-IT applied suggested code changes
1 warnings generated.

While this approach won’t apply constexpr in every case possible, it can safely apply it automatically.

Try it on your code base

Benchmark your tests before and after running constexpr-everything. Not only will your code be faster and smaller, it’ll be safer. Code marked constexpr can’t bitrot as easily.

constexpr-everything is still a prototype – it has a couple of rough edges left. The biggest issue is FixIts only apply to the source (.cpp) files and not to their associated header files. Additionally, constexpr-everything can only mark existing constexpr-compatible functions as constexpr. We’re working on using the machinery provided to identify functions that can’t be marked due to undefined behavior.

The code is available on our GitHub. To try it yourself, you’ll need cmake, llvm and libclang. Try it out and let us know how it works for your project.

One thought on “Use constexpr for faster, smaller, and safer code

  1. There’s an important caveat here: in projects that need to last and to be portable, never mark a function constexpr unless you can prove that for all time and for all platforms, that computation can actually be done at compile time.

    There is no “mutable” escape hatch for constexpr. If some function that’s deeply involved in constexpr computation suddenly can’t be implemented constexpr, you’re completely hosed. (Classic example here: GetPageSize() could be constexpr in some places, but isn’t essentially a constexpr computation.)

Leave a Reply