范围和分解不允许 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);

。效果很好,但没有我想要的那么"详尽"。当然,我可以跟进另外 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,因为正如编译器所述:">__for_begin的值在常量表达式中不可用"。编译器也解释了原因:">注意:__for_begin没有被声明为constexpr"。公平地说,这是一个原因,尽管它可能很愚蠢。
  2. 分解声明不能声明constexpr(紧随其后的是错误的非 constexpr 条件static_assert)。

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

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

我承认,基于范围的for和lambdas一样,实际上只是一个大部分工作,而且大部分是无形的,而不是真正的语言功能——提到__for_begin是对其实现的致命赠品。我也承认,允许正常for循环中的计数器被constexpr通常是棘手的(禁止的),不仅因为它不是恒定的,而且因为原则上你可以在那里有任何类型的表达式,而且它真的不容易提前告诉一般会生成什么值(在编译时不付出合理的努力, 反正)。
另一方面,给定一个精确的有限文本序列(这是尽可能的编译时常量),编译器应该能够进行多次迭代,循环的每次迭代都有一个不同的编译时常量值(如果你愿意,可以展开循环)。不知何故,以可读(非递归模板)的方式,这样的事情应该是可能的吗? 我是不是要求太多了?

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

可能的正确答案是"标准不提供"。

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

我无法回答你的理论问题("这里的语言缺少一个概念吗?","这样的事情应该是可能的吗?我在那里要求太多了吗?","有什么真正的技术原因为什么这不能,不能或不应该工作?这是疏忽、执行缺陷还是故意禁止?但是,从实际的角度来看...

使用递归可变参数模板,可以做到这一点(这是我在缺乏更好的东西的情况下使用的),但在我看来,这是不必要的丑陋。

我认为可变参数模板是正确的方法,并且(您标记为 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); }

最新更新