不允许 constexpr 的范围和分解的原因

Reason for range for and decomposition not allowing constexpr

我想对一对将一个 64 位整数拆分为两个 32 位整数的便捷函数进行一些健全性测试,或者进行相反的操作。这样做的目的是您不会在某处出现错字的情况下再次进行移位和逻辑操作。健全性测试应该可以 100% 确定这对函数,尽管非常微不足道,确实 按预期工作。

没什么特别的,真的......所以我添加的第一件事是:

static constexpr auto joinsplit(uint64_t h) noexcept { auto [a,b] = split(h); return join(a,b); }
static_assert(joinsplit(0x1234) == 0x1234);

... 效果很好,但 "exhaustive" 比我想要的要少。当然,我可以跟进另外 5 或 6 个不同模式的测试,复制粘贴来拯救。但是说真的……让编译器在一个很小的函数中检查十几个值不是很好吗?没有复制粘贴?现在那会很酷。

使用递归可变参数模板,可以做到这一点(这就是我正在使用的,因为缺少更好的东西),但在我看来,它是不必要的丑陋。

考虑到 constexpr 函数和基于范围的 for 的强大功能,拥有像这样的好东西和可读性不是很酷吗:

    constexpr bool test()
    {
        for(constexpr auto value : {1,2,3}) // other numbers of course
        {
            constexpr auto [a,b] = split(value);
            static_assert(value == join(a,b));
        }
        return true; // never used
    }
    static_assert(test()); // invoke test

这个解决方案的一大优点是除了更具可读性之外,从失败中可以明显看出 static_assert 不仅仅是测试 总体上失败了 ,还有失败的确切值。

然而,这不起作用有两个原因:

  1. 您不能将 value 声明为 constexpr,因为正如编译器所述:"The value of __for_begin is not usable in a constant expression"。编译器也解释了原因:"note: __for_begin was not declared constexpr"。很公平,这是一个原因,尽管它可能很愚蠢。
  2. 无法声明分解声明constexpr(紧随其后的是 static_assert 错误的非 constexpr 条件)。

在这两种情况下,我想知道是否真的存在阻碍 constexpr。我明白为什么它不起作用(见上文!),但有趣的问题是 为什么会这样?

我承认将 value 声明为 constexpr 是一个谎言,因为它的值显然不是常数(每次迭代都不同)。另一方面,它所采用的任何值都来自编译时常量值集,但如果没有 constexpr 关键字,编译器将拒绝这样对待它,即 split 的结果是非- constexpr 并且不能与 static_assert 一起使用,尽管它确实可以,无论如何。
好吧,好吧...我可能真的问得太多了,如果我想声明一些具有变化值的东西为常量。即使从某些角度来看,如果 它是 常量,在每次迭代的范围内。不知何故...这里的语言是否缺少概念?

我承认,基于范围的 for 就像 lambda 一样,实际上只是一种大多数情况下有效的 hack,并且大多数情况下是无形的,而不是真正的语言功能——提到 __for_begin 是其实施的死赠品。我也承认,在正常的 for 循环中允许计数器为 constexpr 通常很棘手(禁止),不仅因为它不是常量,而且因为原则上你可以在其中包含任何类型的表达式,并且 确实 不能轻易提前告知通常会生成什么值(无论如何在编译时都需要付出合理的努力)。
另一方面,给定一个精确的有限文字序列(它尽可能地是编译时间常数),编译器应该能够进行多次迭代,循环的每次迭代都有一个不同的编译时间常数值(如果你愿意,展开循环)。不知何故,以可读(非递归模板)的方式,这样的事情应该是可能的? 我是不是要求太多了?

我承认分解声明并不是一件完全 "trivial" 的事情。例如,它可能需要在一个元组上调用 get,这是一个 class 模板(原则上可以是任何东西)。但是,无论如何,get 恰好是 constexpr(所以这不是借口),而且在我的具体示例中,返回了具有两个成员的匿名结构的匿名临时值,因此 public使用直接成员绑定(到 constexpr struct)。
具有讽刺意味的是,编译器甚至 在第一个示例中也做了完全正确的事情 (并且还使用了递归模板)。显然,这是很有可能的。只是,出于某种原因,第二个例子中没有。
再一次,我在这里要求太多了吗?

可能的正确答案是 "The standard doesn't provide that"。

除此之外,是否有任何真正的技术原因导致这不能、不能或不应该 起作用?这是疏忽、实施缺陷还是故意禁止的?

我无法回答你的理论问题(“语言在这里缺少概念吗?”,“这样的事情应该是可能的吗?我在那里要求太多了吗?”,"there any true, technical reasons why this cannot, could not, or should not work? Is that an oversight, an implementation deficiency, or intentionally forbidden?")但是,从实际观点...

With a recursive variadic template, this can be done (and it's what I'm using in lack of something better), but it's in my opinion needlessly ugly.

我认为可变参数模板是正确的方式(你标记了 C++17),使用折叠,没有理由递归化它。

举例

template <uint64_t ... Is>
static constexpr void test () noexcept
 { static_assert( ((joinsplit(Is) == Is) && ...) ); }

下面是一个完整的编译示例

#include <utility>
#include <cstdint>

static constexpr std::pair<uint32_t, uint32_t> split (uint64_t h) noexcept
 { return { h >> 32 , h }; }

static constexpr uint64_t join (uint32_t h1, uint32_t h2) noexcept
 { return (uint64_t{h1} << 32) | h2; }

static constexpr auto joinsplit (uint64_t h) noexcept
 { auto [a,b] = split(h); return join(a, b); }

template <uint64_t ... Is>
static constexpr void test () noexcept
 { static_assert( ((joinsplit(Is) == Is) && ...) ); }

int main()
 {
    test<1, 2, 3>();
 }

-- 编辑 -- 奖励答案

折叠 (C++17) 很棒,但永远不要低估逗号运算符的力量。

您可以在 C++14 中使用辅助函数和未使用数组的初始化获得相同的结果(好吧...完全相同)

template <uint64_t I>
static constexpr void test_helper () noexcept
 { static_assert( joinsplit(I) == I, "!" ); }

template <uint64_t ... Is>
static constexpr void test () noexcept
 {
   using unused = int[];

   (void)unused { 0, (test_helper<Is>(), 0)... };
 }

很明显,在 joinsplit() 中进行了一些改动以使其与 C++14 兼容

static constexpr auto joinsplit (uint64_t h) noexcept
 { auto p = split(h); return join(p.first, p.second); }