何时以及为什么要使用constexpr静态?

时间:2014-10-01 23:07:11

标签: c++ static constexpr

作为免责声明,我在询问前已对此进行过研究。我找到了a similar SO question但是那里的答案感觉有点“稻草人”,并没有真正回答我个人的问题。我也提到了我的方便cppreference page,但这并没有提供对事情的非常“愚蠢”的解释。

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

我看到“constexpr中除了编译时表达式之外不允许任何其他内容”的声明(特别是在SO处)。但是,an article from Bjarne Stroustrup's page在各种示例中解释了实际上constexpr 需要在编译时评估表达式。如果不是,则应生成编译器错误。

我之前的段落似乎有点偏离主题,但它是理解为什么static可以或应该与constexpr一起使用所必需的基线。不幸的是,这个基线有很多矛盾的信息。

任何人都可以帮助我将所有这些信息整合到纯粹的事实中,并提供有意义的示例和概念吗?基本上在了解constexpr真实行为的方式的同时,为什么要使用static呢?通过static constexpr的范围/场景是否有意义,如果它们可以一起使用?

3 个答案:

答案 0 :(得分:14)

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规则下选择 not 分配任何存储来存储声明为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分配存储空间; 可以想象这可能会影响性能的情况(见下文)。

Standardese

变量是对象或引用 [basic] / 6 。 让我们关注对象。

constexpr int a = 42;这样的声明具有明显的简单声明; 它由 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推断出每个对象都有一个地址。 请注意,我们可能无法直接使用此地址,例如如果通过prvalue引用对象。 (甚至有prvalues不是对象,例如文字42。) 两个不同的完整对象必须具有不同的地址 [intro.object] / 6

从这一点开始,我们可以得出结论,声明为constexpr的对象必须具有唯一的地址 任何其他(完整)对象。

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

static and constexpr

恕我直言唯一有趣的问题是“按功能static”,àla

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

据我所知 - 但this seems still not entirely clear - 编译器可能在运行时计算constexpr变量的初始值设定项。 但这似乎是病态的;我们假设那样做, 即它在编译时预先计算初始化程序。

在{em>静态初始化期间完成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对象之间存在明显不同的语义。 类似地,如果地址本身的值是相关的(例如,对于散列图查找)。

答案 1 :(得分:6)

仅限示例。社区维基。

static == per-function(静态存储持续时间)

声明为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的每次(递归)调用的地址都将不同

例如,如果对象很大(例如数组)并且我们需要在需要常量表达式的上下文中使用它(需要编译时常量)并且我们需要获取其地址。 / p>

请注意,由于地址必须不同,对象可能会在运行时初始化 ;例如,如果递归深度取决于运行时参数。初始化程序仍然可以预先计算,但结果可能必须复制到每个递归步骤的新内存区域。 在这种情况下,constexpr仅保证在编译时可以 进行评估,并且在编译时可以 执行初始化那种类型。

static ==每班

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

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

static ==内部联系

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

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

答案 2 :(得分:1)

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

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

示例:

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变量的地址(可能即使你仍然这样做),你的类没有像标准静态const那样的大小增加。