c++ constexpr函数中的静态constexpr变量

inkz8wg9  于 2023-02-17  发布在  其他
关注(0)|答案(2)|浏览(197)

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指令,因为它将缓冲区存储在静态存储器中。而foo2有15条asm指令,因为它需要在每次调用时分配和初始化缓冲区,而编译器无法对此进行优化。
注意foo1在这里只是为了显示foo2中的缺陷。我想写一个在编译和运行时都能使用的函数。这就是foo2背后的想法。但是我们看到它不能像只在运行时使用的foo1那样有效,这是令人不安的。
我找到的唯一有意义的相关讨论是is this,但它没有具体讨论static constexpr
这些问题是:

  • 我的推理正确吗?还是我忽略了static constexpr变量可能导致的一些问题?
  • 是否有任何解决这一问题的建议?
j5fpnvbx

j5fpnvbx1#

我的推理正确吗?还是我忽略了静态constexpr变量可能导致的一些问题?
在处理constexpr变量时,如果允许它们在constexpr上下文中具有静态存储持续时间,则静态存储持续时间必须考虑一些边缘情况。
函数中具有静态存储持续时间的对象仅在 * 第一次进入函数 * 时构造。通常在此时将存储备份应用于常量(对于运行时常量)。如果constexpr上下文中允许static constexpr,则在编译时生成时必须发生以下两种情况之一:

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

由于constexpr在整个上下文中本质上是无状态的,因此在constexpr函数调用期间应用静态存储对象会突然在constexpr调用之间添加状态--这对于constexpr的当前规则来说是一个很大的变化。
C++20也放宽了constexpr的要求,允许析构函数为constexpr,这就带来了更多的问题,比如在上述情况下,析构函数何时必须执行。
我并不是说这不是一个"可以解决的问题";只是现有的语言设施使得在不违反某些规则的情况下解决这个问题有点复杂。
对于自动存储持续时间对象,这更容易理解--因为存储是在某个时间点一致地创建和销毁的。
是否有任何解决这一问题的建议?
据我所知,没有。在谷歌的各个小组里都有关于它的规则的讨论,但是我还没有看到任何关于它的建议。如果有人知道任何建议,请在评论中链接,我会更新我的回答。

变通方案

根据您所需的API和要求,有几种方法可以避免此限制:
1.把常量放到文件作用域中,可能是在detail命名空间下,这使得常量成为全局常量,这可能对你的要求有效,也可能不起作用。
1.将常量抛出到struct/class中的static常量中。如果数据需要模板化,则可以使用此方法,并允许您使用privatefriend来控制对此常量的访问。
1.在包含数据的struct/class上使函数成为static函数(如果这符合您的要求)。
如果数据需要作为模板,这三种方法都可以很好地工作,尽管方法1只适用于C14(C11没有变量模板),而方法2和3可以在C++11中使用。
在我看来,封装方面最干净的解决方案是第三种方法,即把数据和代理函数都移到structclass中,这样可以使数据与功能紧密关联,例如:

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

Compiler Explorer Link
这将生成与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);
}

Compiler Explorer Link.
这再次产生与foo1相同的汇编,同时仍然允许它是constexpr

iqih9akk

iqih9akk2#

我把数组插入到一个非类型的模板参数中:

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标准背后的原因(参见this SO问题)。

相关问题