static constexpr variables in a constexpr function
Asked Answered
M

3

21

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?
Meghanmeghann answered 18/6, 2020 at 19:37 Comment(11)
If you care about run time performance, why not use foo1?Appall
foo1 is here only to show the flaw in foo2, which I want to be available on both compile and run time.Meghanmeghann
@Meghanmeghann At compile time there isn't going to be any assembly. The function call will be replaced with a constant value by the compiler.Appall
@Appall That's why the question is about runtime. I want to write a single function for both, which is the purpose of foo2.Meghanmeghann
@NathanOliver: I think the point is that the assembly generated for foo2_caller is inefficient.Particulate
This doesn't actually answer the questions, but you can always move the array to file-scope, or to a static constexpr member of a struct/class. If the function this is inside of is meant to be a template, then this could be a static constexpr member on a class/struct templateWashko
By the way, foo1 isn't necessarily "runtime-only"; if it's called with a constant argument, then clang and gcc are both able to optimize foo1(4) into a constant, which I suppose was your goal in creating foo2. So foo1 may serve your purpose after all.Particulate
@NateEldredge I believe foo1 still is not allowed to be used in a constexpr context though.Meghanmeghann
@NateEldredge: You cannot do std::array<float, foo1(4)> but you could with foo2.Trimester
static in function does lazy initialization. I think that complicate (too much) implementation of static constexpr. constexpr already had some improvements (relaxation on constraints, ...) since C++11 to be more easy to use.Trimester
@Meghanmeghann I've added some points to my answer regarding static storage duration. tl;dr: this would introduce persisted "state" between constexpr function invocations, which are meant to be stateless. It's not impossible to do, it just that current rules complicate this and would require some large changes to allow for it.Washko
M
4

No longer an issue in C++23, see P2647R1.

Meghanmeghann answered 17/2, 2023 at 14:48 Comment(0)
W
14

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 assembly 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.

Washko answered 18/6, 2020 at 20:16 Comment(4)
"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" - why is this true? How is compile-time storage related to runtime storage?Meghanmeghann
"This would violate existing rules for static storage-duration objects." - I thought storage-duration rules were all about runtime, not about compile time.Meghanmeghann
"suddenly adding state between constexpr invocations" - I would expect that static is simply "ignored" on compile time.Meghanmeghann
"when the destructor must execute in the above cases" - at exactly the same point as it would execute for a global static constexpr variable.Meghanmeghann
I
5

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).

Inapprehensive answered 18/6, 2020 at 20:31 Comment(3)
This is interesting, I've never seen this approach before. The downside would be that this doesn't scale nicely to larger array sizes or to larger sets of constants -- since all initialization is in the template argument list. It also doesn't share across different functions well if this were needed (but then this could be moved to a class template instead or something)Washko
Note that it requires C++20.Trimester
@Human-Compiler: The arguments could be initialized elsewhere with a function or variable if necessary. A class would make sense though if multiple functions needed the same data.Inapprehensive
M
4

No longer an issue in C++23, see P2647R1.

Meghanmeghann answered 17/2, 2023 at 14:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.