Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

static constexpr variables in a constexpr function

static variables are not allowed inside a constexpr function. Which makes sense, since static would introduce a state to a supposed to be pure function.

However, I don't see why we cannot have a static constexpr variable in a constexpr function. It is guaranteed to always have the same value, thus the function would remain pure.

Why would I care? Because static makes a difference at runtime. Consider this code:

#include <array>

constexpr int at(const std::array<int, 100>& v, int index)
{
    return v[index];
}

int foo1(int i) {
    static constexpr std::array<int, 100> v = { 
        5, 7, 0, 0, 5  // The rest are zero
    };
    return at(v, i);
}

constexpr int foo2(int i) {
    constexpr std::array<int, 100> v = { 
        5, 7, 0, 0, 5  // The rest are zero
    };
    return at(v, i);
}

int foo2_caller(int i) {
    return foo2(i);
}

Live: https://gcc.godbolt.org/z/umdXgv

foo1 has 3 asm instructions, since it stores the buffer in the static storage. While foo2 has 15 asm instructions because it is required to allocate and initialize the buffer on each call and the compiler was not able to optimize this away.

Note that foo1 is here only to show the flaw in foo2. I want to write a function that I'll be able to use at both compile and run time. That's the idea behind foo2. But we see it cannot be as efficient as the runtime-only foo1, which is disturbing.

The only meaningful related discussion I found is this, but it does not discuss static constexpr specifically.

The questions are:

  • Is my reasoning correct, or do I miss some problem that static constexpr variables might cause?
  • Are there any proposals to fix this?
like image 949
Mikhail Avatar asked Jun 18 '20 19:06

Mikhail


2 Answers

How's this? I stuck the array into a non-type template parameter:

template<std::array<int, 100> v = {5, 7, 0, 0, 5}>
constexpr int foo2(int i) {
    return at(v, i);
}

On godbolt, foo2's disassembly now matches that of your foo1. This currently works on GCC but not clang; it seems clang is behind the C++20 standard here (see this SO question).

like image 75
clyne Avatar answered Nov 10 '22 15:11

clyne


Is my reasoning correct, or do I miss some problem that static constexpr variables might cause?

There are a few edge-cases that static-storage duration would have to consider when dealing with constexpr variables if they were allowed static-storage duration inside of a constexpr context.

Objects with static storage duration in a function only get constructed on first entry into the function. It is at this time normally that storage-backing is applied to the constants (for runtime constants). If static constexpr were allowed in constexpr context, one of two things has to happen when this is generated at compile-time:

  • Executing the function at compile-time must now generate storage-backing for static constants in case it gets ODR used -- even if it is never used at runtime (which would be non-zero-overhead), or
  • Executing the function at compile-time must now ephemerally create a constant that will be instantiated on each invocation, and finally given storage when a branch calls it with a runtime context (whether or not it's compile-time generated). This would violate existing rules for static storage-duration objects.

Since constexpr is inherently stateless throughout the context, applying a static-storage object during the constexpr function invocation is suddenly adding state between constexpr invocations -- which is a big change for the current rules of constexpr. Though constexpr functions may modify local state, the state is not globally affected.

C++20 also relaxes constexpr requirements to allow for destructors to be constexpr, which raises more questions such as when the destructor must execute in the above cases.

I'm not saying this isn't a solvable problem; it's just that the existing language facilities make solving this a little complicated without violating certain rules.

With automatic storage duration objects, this is much easier to reason about -- since the storage is coherently created and destroyed at a certain point in time.

Are there any proposals to fix this?

None that I am aware of. There have been discussions on various google groups about rules of it, but I have not seen any proposals for this. If anyone knows of any, please link it in the comments and I will update my answer.

Workarounds

There are a few ways you can avoid this limitation depending on what your desired API is, and what the requirements are:

  1. Throw the constant into file scope, perhaps under a detail namespace. This makes your constant global, which may or may not work for your requirements.
  2. Throw the constant into a static constant in a struct/class. This can be used if the data needs to be templated, and allows you to use private and friendship to control access to this constant.
  3. Make the function a static function on a struct/class that contains the data (if this works with your requirements).

All three of these approaches work well if the data needs to be a template, although approach 1 will only work with C++14 (C++11 did not have variable templates), whereas 2 and 3 can be used in C++11.

The cleanest solution in terms of encapsulation, in my opinion, would be the third approach of moving both the data and the acting function(s) into a struct or class. This keeps the data closely associated to the functionality. For example:

class foo_util
{
public:
    static constexpr int foo(int i); // calls at(v, i);
private:
    static constexpr std::array<int, 100> v = { ... };
};

Compiler Explorer Link

This will generate identical assemblies to your foo1 approach while still allowing it to be constexpr.


If throwing the function into a class or struct isn't possible for your requirements (perhaps this needs to be a free function?), then you're stuck either moving the data to file-scope (perhaps protected by a detail namespace convention), or by throwing it into a disjoint struct or class that handles the data. The latter approach can use access modifiers and friendship to control the data access. This solution can work, though it admittedly is not as clean:

#include <array>

constexpr int at(const std::array<int, 100>& v, int index)
{
    return v[index];
}

constexpr int foo(int i);
namespace detail {
    class foo_holder
    {
    private:
        static constexpr std::array<int, 100> v = { 
            5, 7, 0, 0, 5  // The rest are zero
        };
        friend constexpr int ::foo(int i);
    };
} // namespace detail

constexpr int foo(int i) {
    return at(detail::foo_holder::v, i);
}

Compiler Explorer Link.

This, again, produces identical assembly to foo1 while still allowing it to be constexpr.

like image 30
Human-Compiler Avatar answered Nov 10 '22 15:11

Human-Compiler