何时以及为何将静态与 constexpr 结合使用?

2022-01-05 00:00:00 static c++ constexpr

作为免责声明,我在询问之前已经对此进行了研究.我发现了一个类似的问题,但那里的答案感觉有点稻草人"并没有真正为我个人回答这个问题.我还提到了我方便的 cppreference 页面 但这并没有提供大多数时候对事情的解释非常愚蠢".

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.

基本上我仍然在加强 constexpr,但目前我的理解是它需要在编译时评估表达式.由于它们可能仅在编译时存在,因此它们在运行时不会真正拥有内存地址.因此,当我看到人们使用 static constexpr(例如在类中)时,我很困惑...... static 在这里是多余的,因为它只对运行时上下文有用.

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.

我在constexpr 不允许任何东西但编译时表达式"语句中看到了矛盾(特别是在 SO 处).然而,来自 Bjarne Stroustrup 页面的一篇文章 在各种示例中解释了事实上 constexpr 确实 需要在编译时对表达式求值.如果不是,则应生成编译器错误.

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.

我的上一段似乎有点跑题,但它是理解为什么 static 可以或应该与 constexpr 一起使用的必要基础.不幸的是,该基线有很多相互矛盾的信息.

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.

谁能帮我将所有这些信息汇总成纯粹的事实,并附上有意义的示例和概念?基本上除了了解 constexpr 的实际行为之外,你为什么要使用 static 呢?如果它们可以一起使用,static constexpr 在哪些范围/场景中有意义?

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?

推荐答案

constexpr 变量不是编译时值

一个值是不可变的,不占用存储空间(它没有地址),然而,声明为 constexpr 的对象可以是可变的并且确实会占用存储空间(在 as-if 规则下).

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

大多数声明为 constexpr 的对象是不可变的,但是可以定义一个(部分)可变的 constexpr 对象,如下所示:

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
}

存储

在 as-if 规则下,编译器可以选择不分配任何存储空间来存储声明为 constexpr 的对象的值.同样,它可以对非 constexpr 变量进行此类优化.但是,请考虑我们需要将对象的地址传递给未内联的函数的情况;例如:

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);
}

这里的编译器需要为precomputed分配存储空间,以便将其地址传递给某个非内联函数.编译器可以为precomputedi 连续分配存储空间;可以想象这可能会影响性能的情况(见下文).

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

变量要么是对象要么是引用[basic]/6.让我们专注于对象.

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

constexpr int a = 42;这样的声明在语法上是一个简单声明;它由 decl-specifier-seq init-declarator-list ;

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

从 [dcl.dcl]/9,我们可以得出(但不严格)这样的声明声明了一个对象.具体来说,我们可以(严格地)断定它是一个对象声明,但这包括引用声明.另请参阅关于 我们是否可以拥有 void 类型的变量的讨论.

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.

对象声明中的constexpr 暗示对象的类型是const [dcl.constexpr]/9.一个对象是一个存储区域[intro.object]/1.我们可以从 [intro.object]/6 和 [intro.memory]/1 推断每个对象都有一个地址.请注意,我们可能无法直接获取此地址,例如如果对象是通过纯右值引用的.(甚至还有不是对象的纯右值,例如文字 42.)两个不同的完整对象必须有不同的地址[intro.object]/6.

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.

从这一点上,我们可以得出结论,声明为 constexpr 的对象必须具有相对于任何其他(完整)对象.

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

此外,我们可以得出结论,声明 constexpr int a = 42; 声明了一个具有唯一地址的对象.

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

恕我直言,唯一有趣的问题是每个函数static",à la

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

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

据我所知――但这似乎仍然不完全清楚――编译器可能 在运行时计算 constexpr 变量的初始值设定项.但这似乎是病态的;让我们假设它不会那样做,即它在编译时预先计算初始值设定项.

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.

static constexpr 局部变量的初始化在静态初始化期间完成,必须在任何动态初始化[basic.start.init]/2之前执行.虽然不能保证,但我们可以假设这不会造成运行时间/加载时间成本.另外,由于常量初始化没有并发问题,我认为我们可以安全地假设这不需要 线程安全 运行时检查 static 变量是否已经初始化.(查看 clang 和 gcc 的来源应该可以对这些问题有所了解.)

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);
}

结论

看起来,在某些极端情况下,我们可以从 static constexpr 变量的静态存储持续时间中受益.但是,我们可能会丢失此局部变量的局部性,如本答案的存储"部分所示.直到我看到一个基准表明这是一个真实的效果,我会假设这不相关.

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.

如果staticconstexpr对象只有这两种效果,我会默认使用 static :我们通常不需要保证 constexpr 对象的唯一地址.

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.

对于可变的 constexpr 对象(具有 mutable 成员的类类型),本地 static 和非静态 constexpr 对象之间存在明显不同的语义.同样,如果地址本身的值是相关的(例如,对于哈希映射查找).

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

相关文章