首页 > 解决方案 > constexpr 函数中的静态 constexpr 变量

问题描述

static函数内部不允许使用变量constexpr。这是有道理的,因为static会给一个应该是纯函数的状态引入一个状态。

但是,我不明白为什么我们不能在函数中包含static constexpr变量。constexpr保证始终具有相同的值,因此该函数将保持纯净。

我为什么要在乎?因为static在运行时会有所作为。考虑这段代码:

#include <array>

constexpr int at(const std::array<int, 100>& v, int index)
{
    return v[index];
}

int foo1(int i) {
    static constexpr std::array<int, 100> v = { 
        5, 7, 0, 0, 5  // The rest are zero
    };
    return at(v, i);
}

constexpr int foo2(int i) {
    constexpr std::array<int, 100> v = { 
        5, 7, 0, 0, 5  // The rest are zero
    };
    return at(v, i);
}

int foo2_caller(int i) {
    return foo2(i);
}

直播:https ://gcc.godbolt.org/z/umdXgv

foo1有 3 个 asm 指令,因为它将缓冲区存储在静态存储中。Whilefoo2有 15 个 asm 指令,因为需要在每次调用时分配和初始化缓冲区,而编译器无法对此进行优化。

请注意,foo1此处仅显示foo2. 我想编写一个可以在编译和运行时使用的函数。这就是背后的想法foo2。但是我们看到它不能像 runtime-only 那样高效foo1,这令人不安。

我发现的唯一有意义的相关讨论是 this,但没有static constexpr具体讨论。

问题是:

标签: c++staticconstexpr

解决方案


我的推理是否正确,还是我错过了静态 constexpr 变量可能导致的一些问题?

constexpr如果在上下文中允许静态存储持续时间,则在处理变量时,静态存储持续时间必须考虑一些边缘constexpr情况。

函数中具有静态存储持续时间的对象仅在第一次进入函数时被构造。通常在这个时候将存储支持应用于常量(对于运行时常量)。如果static constexprconstexpr上下文中允许,则在编译时生成时必须发生以下两件事之一:

  • 在编译时执行该函数现在必须为静态常量生成存储支持,以防它被使用 ODR——即使它在运行时从未使用过(这将是非零开销),或者
  • 在编译时执行该函数现在必须临时创建一个常量,该常量将在每次调用时实例化,并最终在分支使用运行时上下文调用它时给予存储(无论它是否在编译时生成)。这将违反静态存储持续时间对象的现有规则。

由于constexpr在整个上下文中本质上是无状态的,因此在constexpr函数调用期间应用静态存储对象会突然在constexpr调用之间添加状态——这对于constexpr. 尽管constexpr函数可能会修改本地状态,但状态不会受到全局影响。

C++20 还放宽constexpr了要求,允许析构函数为 constexpr,这引发了更多问题,例如在上述情况下何时必须执行析构函数。

我并不是说这不是一个可以解决的问题;只是现有的语言设施在不违反某些规则的情况下使解决这个问题有点复杂。

使用自动存储持续时间对象,这更容易推理——因为存储是在某个时间点连贯地创建和销毁的。

有没有解决这个问题的建议?

没有我知道的。已经在各种谷歌小组上讨论过它的规则,但我还没有看到任何建议。如果有人知道,请在评论中链接它,我会更新我的答案。

解决方法

有几种方法可以避免此限制,具体取决于您所需的 API 是什么以及要求是什么:

  1. 将常量放入文件范围,可能在detail命名空间下。这使您的常量全局化,这可能会或可能不会满足您的要求。
  2. 将常数投入到/中的static常数中。如果需要对数据进行模板化,则可以使用它,并允许您使用和传送来控制对该常量的访问。structclassprivatefriend
  3. 使函数成为包含数据staticstruct/上的函数(如果这符合您的要求)。class

如果数据需要是模板,这三种方法都可以很好地工作,尽管方法 1 仅适用于 C++14(C++11 没有变量模板),而方法 2 和 3 可以在 C++ 中使用11.

在我看来,就封装而言,最简洁的解决方案是将数据和代理函数都移动到structor中的第三种方法class。这使数据与功能密切相关。例如:

class foo_util
{
public:
    static constexpr int foo(int i); // calls at(v, i);
private:
    static constexpr std::array<int, 100> v = { ... };
};

编译器资源管理器链接

这将生成与您的foo1方法相同的程序集,同时仍然允许它是constexpr.


如果将函数放入 aclassstruct无法满足您的要求(也许这需要是一个自由函数?),那么您要么将数据移动到文件范围(可能受detail命名空间约定保护),要么通过把它扔到一个不相交的地方struct class处理数据的地方。后一种方法可以使用访问修饰符和友谊来控制数据访问。这个解决方案可以工作,尽管它确实不那么干净:

#include <array>

constexpr int at(const std::array<int, 100>& v, int index)
{
    return v[index];
}

constexpr int foo(int i);
namespace detail {
    class foo_holder
    {
    private:
        static constexpr std::array<int, 100> v = { 
            5, 7, 0, 0, 5  // The rest are zero
        };
        friend constexpr int ::foo(int i);
    };
} // namespace detail

constexpr int foo(int i) {
    return at(detail::foo_holder::v, i);
}

编译器资源管理器链接

这再次产生相同的组件,foo1同时仍然允许它是constexpr


推荐阅读