When and why would you use static with constexpr?

2019-01-22 13:16发布

问题:

As a disclaimer, I have done my research on this before asking. I found a similar SO question but the answer there feels a bit "strawman" and didn't really answer the question for me personally. I've also referred to my handy cppreference page but that doesn't offer a very "dumbed down" explanation of things most times.

Basically I'm still ramping up on constexpr, but at the moment my understanding is that it requires expressions to be evaluated at compile time. Since they may only exist at compile time, they won't really have a memory address at runtime. So when I see people using static constexpr (like in a class, for example) it confuses me... static would be superfluous here since that is only useful for runtime contexts.

I've seen contradiction in the "constexpr does not allow anything but compile-time expressions" statement (particularly here at SO). However, an article from Bjarne Stroustrup's page explains in various examples that in fact constexpr does require the evaluation of the expression at compile time. If not, a compiler error should be generated.

My previous paragraph seems a bit off-topic but it's a baseline necessary to understand why static can or should be used with constexpr. That baseline, unfortunately, has a lot of contradicting information floating around.

Can anyone help me pull all of this information together into pure facts with examples and concepts that make sense? Basically along with understanding how constexpr really behaves, why would you use static with it? And through what scopes/scenarios does static constexpr make sense, if they can be used together?

回答1:

constexpr variables are not compile-time values

A value is immutable and does not occupy storage (it has no address), however objects declared as constexpr can be mutable and do occupy storage (under the as-if rule).

Mutability

Most objects declared as constexpr are immutable, but it is possible to define a constexpr object that is (partially) mutable as follows:

struct S {
    mutable int m;
};

int main() {
    constexpr S s{42};
    int arr[s.m];       // error: s.m is not a constant expression
    s.m = 21;           // ok, assigning to a mutable member of a const object
}

Storage

The compiler can, under the as-if rule, choose to not allocate any storage to store the value of an object declared as constexpr. Similarly, it can do such optimizations for non-constexpr variables. However, consider the case where we need to pass the address of the object to a function that is not inlined; for example:

struct data {
    int i;
    double d;
    // some more members
};
int my_algorithm(data const*, int);

int main() {
    constexpr data precomputed = /*...*/;
    int const i = /*run-time value*/;
    my_algorithm(&precomputed, i);
}

The compiler here needs to allocate storage for precomputed, in order to pass its address to some non-inlined function. It is possible for the compiler to allocate the storage for precomputed and i contiguously; one could imagine situations where this might affect performance (see below).

Standardese

Variables are either objects or references [basic]/6. Let's focus on objects.

A declaration like constexpr int a = 42; is gramatically a simple-declaration; it consists of decl-specifier-seq init-declarator-list ;

From [dcl.dcl]/9, we can conclude (but not rigorously) that such a declaration declares an object. Specifically, we can (rigorously) conclude that it is an object declaration, but this includes declarations of references. See also the discussion of whether or not we can have variables of type void.

The constexpr in the declaration of an object implies that the object's type is const [dcl.constexpr]/9. An object is a region of storage[intro.object]/1. We can infer from [intro.object]/6 and [intro.memory]/1 that every object has an address. Note that we might not be able to directly take this address, e.g. if the object is referred to via a prvalue. (There are even prvalues which are not objects, such as the literal 42.) Two distinct complete objects must have different addresses[intro.object]/6.

From this point, we can conclude that an object declared as constexpr must have a unique address with respect to any other (complete) object.

Furthermore, we can conclude that the declaration constexpr int a = 42; declares an object with a unique address.

static and constexpr

The IMHO only interesting issue is the "per-function static", à la

void foo() {
    static constexpr int i = 42;
}

As far as I know -- but this seems still not entirely clear -- the compiler may compute the initializer of a constexpr variable at run-time. But this seems pathological; let's assume it does not do that, i.e. it precomputes the initializer at compile-time.

The initialization of a static constexpr local variable is done during static initializtion, which must be performed before any dynamic initialization[basic.start.init]/2. Although it is not guaranteed, we can probably assume that this does not impose a run-time/load-time cost. Also, since there are no concurrency problems for constant initialization, I think we can safely assume this does not require a thread-safe run-time check whether or not the static variable has already been initialized. (Looking into the sources of clang and gcc should shed some light on these issues.)

For the initialization of non-static local variables, there are cases where the compiler cannot initialize the variable during constant initialization:

void non_inlined_function(int const*);

void recurse(int const i) {
    constexpr int c = 42;
    // a different address is guaranteed for `c` for each recursion step
    non_inlined_function(&c);
    if(i > 0) recurse(i-1);
}

int main() {
    int i;
    std::cin >> i;
    recurse(i);
}

Conclusion

As it seems, we can benefit from static storage duration of a static constexpr variable in some corner cases. However, we might lose the locality of this local variable, as shown in the section "Storage" of this answer. Until I see a benchmark that shows that this is a real effect, I will assume that this is not relevant.

If there are only these two effects of static on constexpr objects, I would use static per default: We typically do not need the guarantee of unique addresses for our constexpr objects.

For mutable constexpr objects (class types with mutable members), there are obviously different semantics between local static and non-static constexpr objects. Similarly, if the value of the address itself is relevant (e.g. for a hash-map lookup).



回答2:

Examples only. Community wiki.

static == per-function (static storage duration)

Objects declared as constexpr have addresses just like any other object. If for some reason, the address of the object is used, the compiler might have to allocate storage for it:

constexpr int expensive_computation(int n); // defined elsewhere

void foo(int const p = 3) {
    constexpr static int bar = expensive_computation(42);
    std::cout << static_cast<void const*>(&bar) << "\n";
    if(p) foo(p-1);
}

The address of the variable will be the same for all invocations; no stack space will be required for it for every function call. Compare to:

void foo(int const p = 3) {
    constexpr int bar = expensive_computation(42);
    std::cout << static_cast<void const*>(&bar) << "\n";
    if(p) foo(p-1);
}

Here, the addresses will be different for every (recursive) invocation of foo.

This matters for example if the object is large (e.g. an array) and we need both to use it in a context where a constant expression is required (requires a compile-time constant) and we need to take its address.

Note that since the addresses must differ, the object might be initialized at run-time; for example, if the recursion depth depends on a run-time parameter. The initializer could still be pre-computed, but the result might have to be copied into the new memory region for each recursion step. In that case, constexpr only guarantees that the intializer can be evaluated at compile-time, and the initialization could be performed at compile-time for a variable of that type.

static == per-class

template<int N>
struct foo
{
    static constexpr int n = N;
};

Same as always: declares a variable for each template specialization (instantiation) of foo, e.g. foo<1>, foo<42>, foo<1729>. If you want to expose the non-type template parameter, you can use e.g. a static data member. It can be constexpr so that other can benefit from the value known at compile-time.

static == internal linkage

// namespace-scope
static constexpr int x = 42;

Pretty much redundant; constexpr variables have internal linkage per default. I don't see any reason currently to use static in this case.



回答3:

I use static constexpr as a replacement for unnamed enums in places where I don't know an exact type definiton, but want to query some information about the type (usually at compile time).

There are some additional benefits to the compile time unnamed enum. Easier debugging (values show up in a debugger like a "normal" variable. Also, you get to use any type that can be constexpr constructed (not just numbers), as opposed to just numbers with an enum.

Examples:

template<size_t item_count, size_t item_size> struct item_information
{
    static constexpr size_t count_ = item_count;
    static constexpr size_t size_ = item_size;
};

Now, you can access these variables at compile time:

using t = item_information <5, 10>;
constexpr size_t total = t::count_ * t::size_;

Alternatives:

template<size_t item_count, size_t item_size> struct item_information
{
    enum { count_ = item_count };
    enum { size_ = item_size };
};

template<size_t item_count, size_t item_size> struct item_information
{
    static const size_t count_ = item_count;
    static const size_t size_ = item_size;
};

The alternatives don't have the all of the positives of static constexpr - you're guaranteed compile time processing, type safety, and (potentially) lower usage of memory (constexpr variables don't need to take up memory, they are effectively hard coded unless if possible).

Unless you start taking the address of constexpr variables (and possibly even if you still do), there is no size increase to your classes like you would see with a standard static const.