在一个constexpr函数中,静态的constexpr变量

20

constexpr函数中不允许使用static变量。这是有道理的,因为static会向本应是纯函数的函数引入状态。

然而,我不明白为什么我们不能在constexpr函数中有一个static 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个汇编指令,因为它将缓冲区存储在静态存储中。而foo2具有15个汇编指令,因为每次调用都需要分配和初始化缓冲区,编译器无法对其进行优化。

请注意,foo1只是为了显示foo2中的缺陷。我想编写一个既能在编译时又能在运行时使用的函数。这就是foo2的想法。但是我们看到它不能像仅限于运行时的foo1那样高效,这很令人不安。

我找到的唯一有意义的相关讨论在这里,但它并没有特别讨论static constexpr

这些问题是:

  • 我的推理正确吗?还是我错过了static constexpr变量可能引起的问题?
  • 有没有修复这个问题的提案?


如果你关心运行时性能,为什么不使用 foo1 呢? - NathanOliver
“foo1” 只是为了展示 “foo2” 中的缺陷,我希望它在编译时和运行时都可用。 - Mikhail
@Mikhail 在编译时不会有任何汇编代码。函数调用将被编译器替换为常量值。 - NathanOliver
1
@NateEldredge 我认为 foo1 仍然不允许在 constexpr 上下文中使用。 - Mikhail
@Mikhail 我在我的答案中添加了一些关于静态存储期的要点。简而言之:这将引入constexpr函数调用之间持久存在的“状态”,而这些函数本来应该是无状态的。这并非不可能实现,只是当前规则会使其变得复杂,并需要进行一些大的更改才能允许它。 - Human-Compiler
显示剩余6条评论
3个回答

14
如果在constexpr上下文中允许static constexpr变量,则具有静态存储期的对象仅在第一次进入函数时构造。这将导致两种情况之一:要么在编译时执行函数生成存储后备,即使它从未在运行时使用(这将产生非零开销),要么在每次调用时临时创建常量,最终在带有运行时上下文的分支调用时给出存储。这将违反现有静态存储期对象的规则。解决此问题的方法有三种:将常量放入文件作用域、将常量放入struct/classstatic常量中,或者将函数作为struct/classstatic函数。这些方法都可以用于模板数据,但是第一个方法只适用于C++14(C++11没有变量模板),而第二个和第三个方法可以在C++11中使用。没有我知道的任何修复这个问题的提案。
class foo_util
{
public:
    static constexpr int foo(int i); // calls at(v, i);
private:
    static constexpr std::array<int, 100> v = { ... };
};

编译器资源链接

这将生成与您的foo1方法完全相同的汇编代码,同时仍然允许它是constexpr


如果将函数放入classstruct中不可能满足您的需求(也许需要成为自由函数?),那么您要么将数据移动到文件作用域(也许受detail命名空间约束的保护下),要么将其放入处理数据的不同structclass中。后一种方法可以使用访问修饰符和友元来控制数据访问。尽管该解决方案并不像前者那样干净,但它仍然有效。

#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


在编译时执行函数现在必须为静态常量生成存储后备,以防止它被 ODR 使用 - 即使它在运行时从未被使用。为什么会这样?编译时存储如何与运行时存储相关? - Mikhail
“这将违反静态存储期对象的现有规则。” - 我认为存储期规则都是关于运行时而不是编译时的。 - Mikhail
1
突然在constexpr调用之间添加状态 - 我期望static在编译时被简单地“忽略”。 - Mikhail
1
在上述情况下,析构函数必须在与全局静态constexpr变量相同的时间点执行。 - Mikhail

5

这样行吗?我将数组放入了一个非类型模板参数中:

template<std::array<int, 100> v = {5, 7, 0, 0, 5}>
constexpr int foo2(int i) {
    return at(v, i);
}

godbolt上,foo2的反汇编现在与您的foo1相匹配。目前,这仅适用于GCC而不是clang;看起来clang在这里落后于C++20标准(请参见SO问题)。

很有趣,我以前从未见过这种方法。缺点是,这不适用于较大的数组大小或较大的常量集,因为所有初始化都在模板参数列表中。如果需要跨不同函数共享,它也不能很好地实现(但是,这可以移到类模板中)。 - Human-Compiler
2
请注意,它需要 C++20。 - Jarod42
@Human-Compiler:如果需要的话,参数可以在其他地方使用函数或变量进行初始化。但是,如果多个函数需要相同的数据,则使用类会更有意义。 - clyne

2

C++23 中不再是问题,请参见 P2647R1


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接