【问题标题】:static constexpr variables in a constexpr functionconstexpr 函数中的静态 constexpr 变量
【发布时间】:2020-10-08 23:43:31
【问题描述】:

static 变量不允许在 constexpr 函数中使用。这是有道理的,因为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 个 asm 指令,因为它将缓冲区存储在静态存储中。而foo2 有 15 条 asm 指令,因为它需要在每次调用时分配和初始化缓冲区,而编译器无法对此进行优化。

注意foo1 在这里只是为了显示foo2 中的缺陷。我想编写一个可以在编译和运行时使用的函数。这就是foo2 背后的想法。但我们发现它的效率不如仅运行时的foo1,这令人不安。

我找到的唯一有意义的相关讨论is this,但并没有专门讨论static constexpr

问题是:

  • 我的推理是否正确,还是我错过了static constexpr 变量可能导致的一些问题?
  • 有解决此问题的建议吗?

【问题讨论】:

  • 如果您关心运行时性能,为什么不使用foo1
  • foo1 在这里只是为了显示foo2 中的缺陷,我希望它在编译和运行时都可用。
  • @Mikhail 在编译时不会有任何程序集。编译器会将函数调用替换为常量值。
  • @NathanOliver 这就是为什么问题是关于运行时的。我想为两者写一个函数,这就是foo2的目的。
  • @NateEldredge 我相信foo1 仍然不允许在constexpr 上下文中使用。

标签: c++ static constexpr


【解决方案1】:

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

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

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

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

由于constexpr 在整个上下文中本质上是无状态的,因此在constexpr 函数调用期间应用静态存储对象会突然在constexpr 调用之间添加状态——这对@987654330 的当前规则来说是一个很大的变化@。尽管constexpr 函数可能会修改本地状态,但该状态不会受到全局影响。

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

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

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

是否有解决此问题的建议?

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

解决方法

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

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

如果数据需要是模板,这三种方法都可以很好地工作,尽管方法 1 仅适用于 C++14(C++11 没有变量模板),而方法 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 命名空间约定保护),或者将其放入处理数据的不相交的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);
}

Compiler Explorer Link.

这再次生成与foo1 相同的程序集,同时仍允许它为constexpr

【讨论】:

  • “在编译时执行函数现在必须为静态常量生成存储支持,以防 ODR 被使用——即使它从未在运行时使用”——为什么这是真的?编译时存储与运行时存储有什么关系?
  • “这将违反静态存储持续时间对象的现有规则。” - 我认为存储持续时间规则都是关于运行时的,而不是关于编译时间的。
  • “在 constexpr 调用之间突然添加状态” - 我希望 static 在编译时被简单地“忽略”。
  • “当析构函数必须在上述情况下执行时” - 与全局静态 constexpr 变量的执行时间完全相同。
【解决方案2】:

这是怎么回事?我将数组粘贴到非类型模板参数中:

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 问题)。

【讨论】:

  • 这很有趣,我以前从未见过这种方法。不利的一面是,这不能很好地扩展到更大的数组大小或更大的常量集——因为所有初始化都在模板参数列表中。如果需要,它也不能很好地在不同的函数之间共享(但可以将其移至类模板或其他东西)
  • 请注意,它需要 C++20。
  • @Human-Compiler:如有必要,可以使用函数或变量在别处初始化参数。如果多个函数需要相同的数据,则一个类会有意义。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-04-20
  • 2012-12-01
  • 2019-05-25
  • 2020-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多