为什么这个嵌套的lambda表达式不被视为constexpr?

7

我试图使用嵌套的constexpr lambda创建柯里化接口,但编译器认为它不是常量表达式。

namespace hana = boost::hana;
using namespace hana::literals;

struct C1 {};

template < typename T,
           std::size_t size >
struct Array {};

constexpr auto array_ = [] (auto size) {
      return [=] (auto type) {
        return hana::type_c<Array<typename decltype(type)::type, size()>>;
      };
    };

int main() {

  constexpr auto c1 = hana::type_c<C1>;
  constexpr auto test = hana::type_c<Array<typename decltype(c1)::type, hana::size_c<100>()>>;
  constexpr auto test2 = array_(hana::size_c<100>)(c1);
}

我之前发布了一个问题,因为我找到了一个不同的最小化示例,但那还不够。

错误:

test2.cpp: In instantiation of ‘<lambda(auto:1)>::<lambda(auto:2)> [with auto:2 = boost::hana::type_impl<C1>::_; auto:1 = boost::hana::integral_constant<long unsigned int, 100>]’:
test2.cpp:31:54:   required from here
test2.cpp:20:16: error: ‘__closure’ is not a constant expression
         return hana::type_c<Array<typename decltype(type)::type, size()>>;
                ^~~~
test2.cpp:20:16: note: in template argument for type ‘long unsigned int’ 
test2.cpp: In function ‘int main()’:
test2.cpp:31:18: error: ‘constexpr const void test2’ has incomplete type
   constexpr auto test2 = array_(hana::size_c<100>)(c1);

__closure不是常量表达式:如果有人能解释一下这个错误,那就太好了。我之前也遇到过这个错误,但是不记得为什么了。


捕获非constexpr值的lambda不是constexpr。在这种情况下,size是一个非constexpr函数参数。有一个简单的解决方法。询问为什么lambda调用的结果不是constexpr,也许他们会取消标记它。 - Jason Rice
3
我相信这个问题比“是否支持constexpr lambda”更具体,而且它解决了这类元编程中的一个常见陷阱。 - Jason Rice
@JasonRice 你的意思是 hana::size_c<100> 因为 lambda 捕获而失去了它的 constexpr 吗?我稍微休息一下,然后看看能否找到这个解决办法,谢谢 :) - Mathieu Van Nevel
2
它不是constexpr,因为它是一个函数参数。由于它是无状态的且具有默认构造函数,您可以执行constexpr auto size_ = decltype(size){};或使用类型别名并在没有捕获的嵌套lambda上下文中使用Size{},这更好。请注意,事先将所有内容都设置为constexpr并不总是必要的。 - Jason Rice
2个回答

6

我将您的测试用例简化为以下内容:

#include <type_traits>

constexpr auto f = [](auto size) {
  return [=](){
    constexpr auto s = size();
    return 1;
  };
};

static_assert(f(std::integral_constant<int, 100>{})(), "");

int main() { }

如上评论所述,这是因为size不是函数体内的常量表达式。这不是Hana特有的问题。作为解决方法,您可以使用

constexpr auto f = [](auto size) {
  return [=](){
    constexpr auto s = decltype(size)::value;
    return 1;
  };
};

或类似的任何内容。


我对这种行为很感兴趣,你知道在哪里可以找到相关信息吗?为什么函数参数意味着它不是constexpr? - Mathieu Van Nevel
1
@MathieuVanNevel Boost.Hana的手册中有关于此的附录:http://boostorg.github.io/hana/index.html#tutorial-appendix-constexpr - Jason Rice

5
问题在于您试图在模板非类型参数中使用lambda捕获的变量之一,从而导致odr-use。
  return hana::type_c<Array<typename decltype(type)::type, size()>>;
//                                                         ^~~~

模板的非类型参数必须是一个常量表达式。在 lambda 中,你不能在常量表达式中使用一个被捕获的变量。无论 lambda 是否为 constexpr 都是无关紧要的。

但是你可以在常量表达式中使用普通变量的 odr-use,即使它们不是 constexpr 变量。例如,这是合法的:

std::integral_constant<int, 100> i; // i is not constexpr
std::array<int, i()> a; // using i in a constant expression

所以为什么我们不能在常量表达式中重复使用捕获的变量呢?我不知道这个规则的动机,但是标准中有这样一条规定:
[expr.const] (¶2) 除非... (¶2.11) 在lambda表达式中,引用this或定义在该lambda表达式之外具有自动存储期的变量的引用将是odr-use,否则条件表达式是核心常量表达式。 CWG1613 可能会提供一些线索。
如果我们将内部lambda重写为一个命名类,我们将面临一个不同但相关的问题:
template <typename T>
struct Closure {
  T size;
  constexpr Closure(T size_) : size(size_) {}

  template <typename U>
  constexpr auto operator()(U type) const {
    return hana::type_c<Array<typename decltype(type)::type, size()>>;
  }
};
constexpr auto array_ = [] (auto size) {
  return Closure { size };
};

现在的错误是在模板非类型参数中隐式使用了this指针。
  return hana::type_c<Array<typename decltype(type)::type, size()>>;
//                                                         ^~~~~

我将Closure::operator()()声明为constexpr函数以保持一致,但这并不重要。在常量表达式中禁止使用this指针([expr.const] ¶2.1)。声明为constexpr的函数不能获得放宽规则的特权,以便让其中可能出现的常量表达式放松限制。
现在,原始错误有点更加合理了,因为捕获的变量被转换为lambda闭包类型的数据成员,所以使用捕获的变量有点像通过lambda自己的"this指针"间接引用。
这是引入最小代码更改的解决方法:
constexpr auto array_ = [] (auto size) {
  return [=] (auto type) {
    const auto size_ = size;
    return hana::type_c<Array<typename decltype(type)::type, size_()>>;
  };
};

现在我们正在使用捕获的变量来初始化普通变量,在模板非类型参数中使用该变量。请注意,此答案已进行了几次编辑,因此下面的评论可能引用先前的修订版本。

1
你确定 integral_constant::operator() 是否对 *this 执行 lvalue-to-rvalue 转换是未定义的吗?如果是这样,那么这个答案是正确的,我的答案就是错误的。 - Louis Dionne
1
@LouisDionne 我无法找到任何迹象表明它可能应用L2R转换。也许我在看错地方。但我正在重新思考我的答案的其他部分。我认为我误解了积分常数表达式。我认为gcc和clang不同的地方在于lambda是否odr使用“size”。 - Oktalist
@LouisDionne 我现在想我搞清楚了,谢谢。 - Oktalist

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