【问题标题】:When and why would you use static with constexpr?何时以及为什么将静态与 constexpr 一起使用?
【发布时间】:2014-11-26 22:14:57
【问题描述】:

作为免责声明,我在询问之前已经对此进行了研究。我找到了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 在哪些范围/场景下有意义,如果它们可以一起使用?

【问题讨论】:

  • “由于它们可能只存在于编译时,它们在运行时不会真正拥有内存地址。” 它们的行为(可以行为)就像const 对象在运行时。
  • constexpr for variables 要求初始化程序在编译时可评估,以便变量本身可以在必须在编译时评估/评估的上下文中使用。变量的类型将隐式变为const。我想就是这样。
  • @dyp 如果它们在运行时可以表现得像 const ,那么这在哪里记录?你能找到任何来源吗(C++11 草案会很好,我在其中找不到相关文本)
  • [dcl.constexpr] 小节主要定义了constexpr 说明符的行为。例如,[dcl.constexpr]/9(在 N3797 中)表示“对象声明中使用的 constexpr 说明符将对象声明为 const。”包含constexpr 的变量声明基本上是对象的普通声明,但有一些附加规则。
  • +1 由于 Bjarne 和 Herb 等专家所说的与标准实际保证的内容之间的冲突,这个问题的答案有些复杂。这在我的answer here, which is also on constexpr 的cmets 中得到了很好的证明。我的感觉是这需要一点时间才能弄清楚,我花了很多时间试图收集有关 C++11/14 良好实践的详细信息,尽管有一些很好的资源,但在某些主题上可能很困难。跨度>

标签: c++ static constexpr


【解决方案1】:

仅作为示例。社区维基。

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 的每次(递归)调用,地址都会不同

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

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

static == 每类

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

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

static == 内部链接

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

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

【讨论】:

  • 我知道如何使用静态和不同的含义。您的答案根本没有真正解决constexpr ::: (1) 为什么在这个例子中甚至使用constexprstatic const char my_str[] ... 会根据你的解释做同样的事情。 (2) 为什么还要在那里使用static? (3) 除了 constexpr,static 成员变量 为每个实例化声明。它们被声明 1 次并为所有相同类型的实例共享。
  • Ad 1) 嗯,我想我已经在描述中提到了;似乎需要澄清。广告3)实例化=模板实例化,而不是类->对象实例化。
  • 我认为对我来说困难的部分是 constexpr 感觉应该只在编译时完成。感觉就像常量表达式的结果应该总是被内联,因此意味着 constexpr 变量或函数在运行时不会真正存在,只有它们的结果。我们已经有了 const 并且可以根据需要直接使用它,我看不出为什么 constexpr 必须“表现相同”,本质上,恕我直言,这使得 constexpr 毫无意义并且自我挫败。
  • @void.pointer constexpr 要求初始化器可以在编译时计算。与const 本身一样,它(也)是程序员 的注释,以便编译器可以检查该承诺是否已被破坏。同样constexpr 函数:编译器可以在编译时评估(一些)非 constexpr 函数,但这只是非强制性优化。 常量表达式(包括constexpr函数)是编译时评估的“编译器支持”的最小公分母,程序员可以在需要编译时常量的地方轻松使用它们
  • 也许不是一个非常有力的理由,但在 C11(不是 C++)中,隐式 static 已被弃用(第 6.11.2 节)。一般来说,由于static 对象确实具有每个 TU 不同地址的惊人属性,因此最好明确说明它。
【解决方案2】:

我使用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;
};

替代方案没有 static constexpr 的所有优点 - 可以保证编译时处理、类型安全和(可能)降低内存使用率(constexpr 变量不需要占用内存,它们除非可能,否则会被有效地硬编码)。

除非您开始获取 constexpr 变量的地址(并且可能即使您仍然这样做),否则您的类不会像使用标准静态 const 看到的那样增加大小。

【讨论】:

  • 只要您不定义那些static constexpr变量(在类主体之外),它们就不会占用空间(只要您使用兼容的编译器)。 Standardese:它们没有定义,因此它们可能不会被 odr-used。
【解决方案3】:

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; 这样的声明在语法上是一个简单声明; 它由 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”,à la

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

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

static constexpr 局部变量的初始化在静态初始化期间完成, 这必须在任何动态初始化[basic.start.init]/2之前执行。 尽管不能保证,但我们可以假设这不会产生运行时/加载时成本。 此外,由于常量初始化不存在并发问题, 我认为我们可以放心地假设这不需要 thread-safe 运行时检查 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 对象之间存在明显不同的语义。 同样,如果地址本身的值是相关的(例如,对于哈希映射查找)。

【讨论】:

  • 我认为可以预先计算 constexpr 对象的初始化器,然后(mem)将该值复制到对象,例如在recurse 函数中。即,如果对象很大,这只是一个巨大的成本。
  • 你的意思是non_inlined_function(&amp;c);
  • @Mikhail 谢谢,已修复。
猜你喜欢
  • 1970-01-01
  • 2017-03-26
  • 2015-05-04
  • 1970-01-01
  • 1970-01-01
  • 2010-12-09
  • 2014-05-27
  • 1970-01-01
  • 2011-11-07
相关资源
最近更新 更多