7

I'm trying to create a curried interface using nested constexpr lambdas, but the compiler does not consider it to be a constant expression.

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);
}

I post a question earlier because I found a different minimal example, but it wasn't enough.

Error:

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 is not a constant expression : if someone could explain me this error that would be a great help. I ran into that error before but can't remember why.

4
  • A lambda that captures a non-constexpr value is not constexpr. In this case size is a non-constexpr function parameter. There is a simple workaround for this. Ask why the result of the lambda call is not constexpr and maybe they will unflag it. Commented Apr 27, 2017 at 20:35
  • 3
    @NathanOlivier I believe this question is more specific than "Is constexpr lambda supported", plus it addresses a common pitfall with this type of metaprogramming. Commented Apr 27, 2017 at 20:55
  • @JasonRice Do you mean that hana::size_c<100> "lost" it's constexpr because of the lambda capture? I'm going to sleep a little and I'll check if I can find this workaround, tanks :) Commented Apr 27, 2017 at 22:15
  • 2
    It's not constexpr because it is a function parameter. Since it is stateless and default constructible you can do constexpr auto size_ = decltype(size){}; or use a type alias and have it in the context of the nested lambda without capture Size{} which is better. Note that having everything constexpr upfront isn't always necessary. Commented Apr 27, 2017 at 22:23

2 Answers 2

6

I reduced your test case to this:

#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() { }

As said in the comments above, this happens because size is not a constant expression from within the function body. This is not specific to Hana. As a workaround, you can use

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

or anything similar.

Sign up to request clarification or add additional context in comments.

2 Comments

I'm interrested by this behavior, do you know where I can find some information on this? Why it's a function parameter means that it's not constexpr?
@MathieuVanNevel Boost.Hana's manual has an appendix on this: boostorg.github.io/hana/index.html#tutorial-appendix-constexpr
5

The problem is you are trying to odr-use one of a lambda's captured variables in a template non-type argument.

  return hana::type_c<Array<typename decltype(type)::type, size()>>;
//                                                         ^~~~

A template non-type argument must be a constant expression. Inside a lambda, you can't odr-use a captured variable in a constant expression. Whether or not the lambda is constexpr is irrelevant.

But you can odr-use ordinary variables in constant expressions, even if they are not constexpr variables. For example, this is legal:

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

So why can't we odr-use captured variables in constant expressions? I don't know the motivation for this rule, but here it is in the standard:

[expr.const]

(¶2) A conditional-expression is a core constant expression unless... (¶2.11) in a lambda-expression, a reference to this or to a variable with automatic storage duration defined outside that lambda-expression, where the reference would be an odr-use.

CWG1613 may hold some clue.

If we were to rewrite the inner lambda as a named class, we would have a different but related problem:

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 };
};

Now the error would be the implicit use of the this pointer in a template non-type argument.

  return hana::type_c<Array<typename decltype(type)::type, size()>>;
//                                                         ^~~~~

I declared Closure::operator()() as a constexpr function for consistency, but that is immaterial. The this pointer is forbidden to be used in a constant expression ([expr.const] ¶2.1). Functions declared constexpr do not get special dispensation to relax the rules for the constant expressions that may appear within them.

Now the original error makes a little bit more sense, because captured variables are transformed into data members of the lambda's closure type, so using captured variables is a little bit like indirecting through the lambda's own "this pointer".

This is the workaround that introduces the least alteration to the code:

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

Now we are using the captured variable outside of a constant expression, to initialize an ordinary variable which we can then use in the template non-type argument.

This answer has been edited a few times, so the comments below may reference previous revisions.

3 Comments

Are you sure it is not implementation-defined whether integral_constant::operator() performs an lvalue-to-rvalue conversion on *this? If so, then this answer is the correct one and my answer is wrong.
@LouisDionne I wasn't able to find any indication that it might apply the L2R conversion. Maybe I'm looking in the wrong place. But I am rethinking other parts of my answer. I think I misunderstood integral constant expression. I think that where gcc and clang differ is on whether or not the lambda odr-uses size.
@LouisDionne I think I figured it out now, thank you.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.