c++ 什么时候以及为什么要在constexpr中使用static?

lb3vh1jj  于 2023-05-20  发布在  其他
关注(0)|答案(3)|浏览(257)

作为免责声明,我在问之前已经做了我的研究。我找到了a similar SO question,但那里的答案感觉有点“稻草人”,并没有真正回答我个人的问题。我也提到了我的方便cppreference page,但大多数时候它并没有提供一个非常“简单”的解释。
基本上,我仍然在增加constexpr,但目前我的理解是,它需要在编译时计算表达式。因为它们可能只存在于编译时,所以它们在运行时实际上没有内存地址。所以当我看到人们使用static constexpr时(比如在课堂上),我很困惑。static在这里是多余的,因为它只对运行时上下文有用。
我在“constexpr不允许任何东西,但编译时表达式”语句中看到了矛盾(特别是在SO这里)。然而,Bjarne Stroustrup的页面上的一篇文章在各种示例中解释说,实际上constexpr * 确实 * 需要在编译时对表达式求值。如果不是,则应生成编译器错误。
我的前一段似乎有点离题,但它是理解为什么static可以或应该与constexpr一起使用的基础。不幸的是,这个基线有很多相互矛盾的信息。
有谁能帮我把所有这些信息整合成有意义的例子和概念的纯粹事实吗?基本上沿着除了理解constexpr的实际行为之外,为什么还要使用static呢?如果可以一起使用,static constexpr在哪些作用域/场景中有意义?

iovurdzv

iovurdzv1#

函数级静态变量有一个显著的区别,这与lambda-capture有关:

void odr_use(int const&);

int main() {
    int non_static = 42;
    static int is_static = 42;
    []{
        odr_use(non_static); // error
        odr_use(is_static);  // OK
    }();
}

你可以在lambda表达式中odr-使用函数局部静态变量而不捕获它们。这与constexpr无关-但是,强制捕获constexpr变量通常没有什么意义。因此,static + constexpr在从lambdas访问常量时提供了更多的舒适性,考虑:

#include <string_view>
int main()
{
    constexpr std::string_view x = "foo";
    []{ x.data(); }; // error: odr-use of non-captured variable
}

在本例中,字符串视图及其内容是常量。然而,成员函数的使用会触发odr-use,这需要我们捕获变量。或者,使用static + constexpr
Odr-use的意思是“根据一定义规则使用”,它可以归结为“是该操作所需的对象的地址”。对于成员函数,需要地址来形成this指针。
下面你会发现概念上的差异,这也解释了上面提到的效果。

constexpr变量不是编译时值

值是不可变的,不占用存储空间(它没有地址),但是声明为constexpr的对象可以是可变的,并且占用存储空间(在as-if规则下)。

可变性

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

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变量进行这样的优化。但是,考虑一下这样的情况:我们需要将对象的地址传递给一个没有内联的函数;例如:

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连续分配存储空间;可以想象这可能影响性能的情况(见下文)。

标准语

变量可以是对象或引用[basic]/6。让我们专注于物体。
constexpr int a = 42;这样的声明在语法上是一个 simple-declaration;它由 decl-specifier-seq**init-declarator-list;组成
从[dcl.dcl]/9中,我们可以得出结论(但不是严格地),这样的声明声明了一个对象。具体来说,我们可以(严格地)得出结论,它是一个 * 对象声明 *,但这包括引用的声明。另请参阅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。
从这一点上,我们可以得出结论,声明为constexpr的对象必须相对于任何其他(完整)对象具有唯一的地址。
此外,我们可以得出结论,声明constexpr int a = 42;声明了具有唯一地址的对象。

static和constexpr

恕我直言,唯一有趣的问题是“per-function static

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

据我所知--但是this seems still not entirely clear--编译器 * 可能 * 在运行时计算constexpr变量的初始化器。但这似乎是病态的;让我们假设它不做这件事,即。它在编译时预先计算初始化器。
static constexpr局部变量的初始化是在 * 静态初始化 * 期间完成的,它必须在任何 * 动态初始化 *[basic.start.init]/2之前执行。虽然不能保证,但我们可以假设这不会强加运行时/加载时成本。此外,由于常量初始化没有并发问题,我认为我们可以安全地假设这不需要 * 线程安全 * 运行时检查static变量是否已经初始化。(研究clang和gcc的来源应该能对这些问题有所帮助。)
对于非静态局部变量的初始化,存在编译器在常量初始化期间无法初始化变量的情况:

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变量的静态存储持续时间中受益。但是,我们可能会丢失这个局部变量的局部性,如本答案的“存储”部分所示。在我看到一个基准测试表明这是一个真实的效果之前,我会假设这是不相关的。
如果staticconstexpr对象上只有这两个效果,我会默认使用static:我们通常不需要保证constexpr对象的唯一地址。

对于可变的constexpr对象(具有mutable成员的类类型),在本地static和非静态constexpr对象之间存在明显不同的语义。类似地,如果地址本身的值是相关的(例如,用于散列图查找)。

yws3nbqq

yws3nbqq2#

仅为示例。社区wiki

static ==每个函数(静态存储时间)

声明为constexpr的对象与任何其他对象一样具有地址。如果由于某种原因,使用了对象的地址,编译器可能必须为它分配存储空间:

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

变量的地址对于所有调用都是相同的;对于每个函数调用都不需要堆栈空间。比较对象:

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

在这里,对于foo的每次(递归)调用,地址都是不同的。
例如,如果对象很大(例如,一个数组),我们需要在需要常量表达式(需要编译时常量)的上下文中使用它,并且我们需要获取它的地址。
注意,由于地址必须不同,对象可能在运行时初始化*;例如如果递归深度取决于运行时参数。初始化器仍然可以预先计算,但是结果可能必须复制到每个递归步骤的新内存区域中。在这种情况下,constexpr只保证初始化器 * 可以 * 在编译时求值,并且初始化 * 可以 * 在编译时为该类型的变量执行。

static ==每类

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

一如既往:为foo的每个模板特化(示例化)声明一个变量,例如foo<1>foo<42>foo<1729>。如果你想公开非类型的模板参数,你可以使用例如。静态数据成员。它可以是constexpr,以便其他人可以从编译时已知的值中受益。

static ==内部链接

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

相当多余; constexpr变量默认具有内部链接。在这种情况下,我认为目前没有任何理由使用static

k4emjkb1

k4emjkb13#

***我使用static constexpr作为未命名枚举的替代品,在我不知道确切的类型定义的地方,但希望查询有关类型的一些信息(通常在编译时)。

编译时未命名枚举还有一些额外的好处。更容易调试(值在调试器中显示为“普通”变量。此外,您可以使用任何可以由constexpr构造的类型(不仅仅是数字),而不仅仅是带有枚举的数字。
示例:

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

现在,您可以在编译时访问这些变量:

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

备选方案:

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

替代方案没有静态constexpr的所有优点-保证编译时处理,类型安全和(潜在的)更低的内存使用(constexpr变量不需要占用内存,除非可能,否则它们是有效的硬编码)。
除非你开始获取constexpr变量的地址(甚至可能你仍然这样做),否则类的大小不会像你在标准静态常量中看到的那样增加。

相关问题