Why is it possible to use the return of a lambda, passed as argument of a constexpr function argument, in a constant expression within the function?
Asked Answered
H

2

9

The wording of the question title is probably incorrect and I'll fix it happily from your suggestions.

My question can be illustrated by this snippet:

#include <array>

template <typename Callable>
constexpr auto make_array_ok(Callable callable) {
    return std::array<int, callable()>{};
};
// constexpr auto make_array_bad(std::size_t s)
// {
//     return std::array<int,s>{};
// };

// straightforward constexpr function returning its arg
template <std::size_t S>
constexpr std::size_t foo() {
    return S;
}

int main(int argc, char**) {
    static_cast<void>(argc);

    auto size = []() { return std::size_t{42}; };

    // fails to compile -- as I expected
    // auto size_dyn = [argc]() { return std::size_t(argc); };
    // [[maybe_unused]] auto a = make_array_ok(size_dyn);

    // also fails to compile -- but why?
    [[maybe_unused]] auto size_capt = [arg = size()]() { return arg; };
    // [[maybe_unused]] auto b = make_array_ok(size_capt);
    // also fails to compile -- but why?
    [[maybe_unused]] constexpr auto size_capt_ce = [arg = size()]() {
        return arg;
    };
    // [[maybe_unused]] auto c = make_array_ok(size_capt_ce);

    // direct usage also fails to compile -- is it because the closure is not
    // constexpr? auto d = std::array<int,size_capt()>{}; direct usage compiles
    // when the closure is constexpr
    [[maybe_unused]] auto e = std::array<int, size_capt_ce()>{};
    // Simpler exemple with a constexpr function instead of a lambda closure.
    [[maybe_unused]] auto f = std::array<int, foo<42>()>{};
    // calling the constexpr function through a function fails to compile - but
    // why?
    // [[maybe_unused]] auto g = make_array_ok(foo<42>);

    // compiles with captureless lambda
    [[maybe_unused]] auto h = make_array_ok(size);
    return size_capt_ce();
}

Live

It's quite common that constexpr function arguments are considered as not usable where a constant expression is expected (as constexpr function can be called at runtime, with runtime argument), so the failure of make_array_bad to compile is expected. But under what rules can make_array_ok compile (when used with the lambda returning 42)?

I'm suspecting that it's because lambda's operator() is constexpr by default from C++17 on, but I didn't find details in cppreference about constexpr function about the usability of its argument in constant expressions.

But then why does it not work with the size_capt version?


[EDIT] I updated the example above with a constexpr function instead of a lambda and showing the difference between a direct usage and an usage through a function call. I hope it helps clarifying the issue and can be used to improve the (already interesting) provided answers.

Is it the callable copy (when passed as argument) that is breaking the requirements for constant expression?

Harness answered 29/2 at 16:14 Comment(9)
This boils down to that the first lambda doesn't have a state while the second one does. That is, the first one is empty while the second one isn't.Siclari
The first lambda is implicitly constexpr and the second isn'tSollie
en.cppreference.com/w/cpp/language/…(*)(params)() => "This function is constexpr if the function call operator (or specialization, for generic lambdas) is constexpr." ? The lambda, if capture-less is implicitly convertible to a pointer to constexpr function?Harness
the thingy that is lambda specific is "operator() is always constexpr if it satisfies the requirements of a constexpr function. It is also constexpr if the keyword constexpr was used in the lambda specifiers. " (same page)Chapter
Does this answer your question? https://mcmap.net/q/1095491/-constexpr-member-functions-that-don-39-t-use-this/4117728Chapter
my previous comment pointed to the wrong part of the doc but it's fixed in the provided answersHarness
Is "KO" a typo for "OK"?Degeneration
@Barmar: The OP seems to be using "KO" to mean "not OK". This is my first time encountering this use, but it seems to have some currency among French– and Italian-speakers: https://mcmap.net/q/1173855/-why-jmeter-failures-marks-as-ko/978917 . Probably the OP speaks some language where this usage is widely understood, and didn't realize that the same is not the case in English.Demagnetize
@Degeneration My bad, I've encountered many time the use of "KO" as "not OK". I didn't realize it was uncommon. I found it "nice" as it also mean "Knocked Out" in boxing :)Harness
M
9

Answer to the original question.

The reason why that code works is in the comments, but it maybe useful to share the thought process one could follow to get to the answer:

  • Template arguments must be known at compile time,
  • therefore if std::array<int, callable()> compiles, callable() must be known at compile time,
  • which means that callable is a constexpr callable (e.g. a constepxr function or the constexpr operator() of a lambda);
  • since you know you're passing a lambda as the callable, the question becomes: why is operator() constexpr for []() { return std::size_t{42}; } but not for [argc]() { return std::size_t(argc); }? And the answer is the other answer :D

Answer to the updated question

I think it all boils down to a much simpler example: why does the following fail to compile?

#include <array>

constexpr auto lambda = [arg = 0]() constexpr { return arg; };

constexpr auto make_array(decltype(lambda) callable) {
    return std::array<int, callable()>{};
};

where 0 is definitely a constant expression, just like size(), but with less room for doubts.

The point is just that function parameters are not constexpr, in the sense that across function boundaries they lose constexprness, and so even the state of the lambda, which is part of the object, not part of its type (just like the 0 in Foo{} given struct Foo { int i{0}; };, but unlike, say, the size of a std::array, which is embedded in the type itself), can't be used in a constant expression.

One more detail about the sub-question...

... as to why make_array_ok(foo<42>) fails to compile, it's useful to look at it from this perspective: The type of foo<42> is std::size_t(). Yes, it is constexpr, but it will cause make_array_ok to be instantiated with Callable being std::size_t(*)() ((*) is because of function to pointer-to-function decay).

Essentially, passing foo<42> to make_array_ok has caused the following instantiation:

constexpr auto make_array_ok(std::size_t(*callable)()) {
    return std::array<int, callable()>{};
};

Now, the question to ask oneself is: what would the instatiation be, if I called make_array_ok(foo<43>)?

Well, that's easy, because foo<43> has the same type as foo<42>: it's the same instatiation! And you can't possibly have 1 instatiation to return different values depending what the value of callable is.

This shows that unless the value returned by the argument to make_array_ok is part of the type of that argument, that value will not be available in make_array_ok's body at compile-time, but only at run-time.

When the value returned by the argument passed through callable is part of callable's type, as it happens when you pass a stateless lambda returning a literal, then yeah, that value is available in make_array_ok's body because it can be retrieved from the type bound to that specific instantiation of make_array_ok.

Micronesia answered 29/2 at 16:34 Comment(9)
Thanks for the details. That seems to support one of my comments: I can't see anything obvious that prevent the size_cap lambda above to being constexpr (btw, the compilers are accepting it), without needing to declare it so explicitly and it's operator() return should be accepted as a NTTP. It may be that @Andy is right and it is a compiler(s) bug.Harness
@Oersted, compilers are accepting what?Micronesia
yes. I can declare my lambda constexpr and use it in a non constant expression.Harness
Do you have any explanation for the compilation failure with foo<42>?Harness
@Oersted, if you shrink foo down to constexpr std::size_t foo() { return 42; }, that would also fail to compile. The reason is fundamentally the same, I believe: 42 is not part of the type, so it can't pass through a function boundary without becoming runtime-y. Indeed, Callable is deduced to be std::size_t(*)(), and its pointee is not known at compile time. See this example.Micronesia
OK, I think I understand the argument.Harness
@Oersted, I've also added a possibly simpler view on the matter.Micronesia
I don't know if it deserve another question but if I'm making make_array_ok consteval, I would have expected that, necessarily, it's arguments can be considered as constant expression though making auto c = make_array_ok(size_capt_ce); valid. It's not the case (even with constexpr auto c btw).Harness
Answering to myself: #56131292Harness
S
5

The first lambda is implicitly constexpr while the second one is not. This can be seen from lambda expression's documentation:

Specifier Effect
constexpr (since C++17) Explicitly specifies that operator() is a constexpr function. If operator() satisfy all constexpr function requirements, operator() will be constexpr even if constexpr is not present.

And since the operator() for first lambda satisfies all requirements of constexpr function, its operator() is implicitly constexpr.

Siclari answered 29/2 at 16:32 Comment(12)
Thx, but I'm missing something. What requirement is broken? en.cppreference.com/w/cpp/language/…. Is it mere the presence of a capture (I tried capturing a compile time value and the code is broken also).Harness
@Oersted: The value of argc is not known at compile-time, so the lambda cannot be constructed at compile-time. Its operator() also relies on argc, whose value is not known at compile-time, so it cannot be constexprSollie
@Sollie in "capturing a compile time value" I tried to make a capture-full lambda initialized with a constant expression and it cannot be used either to create the array.Harness
I updated the question for all to see this new point about a lambda with capture.Harness
@Oersted: Hmm, I'm leaning towards compiler bug. With enough trickery I can get gcc to accept it, but not MSVC or clang. Conversely, with other trickery I can get MSVC to accept it, but not gcc or clang.Sollie
I still don't understand why, in the capturing version, the constexpr requirements are not fullfilled. In this snippet about evaluating lambas as NTTP I'm showing that lambda closures can be used as NTTP as long as they are at least constexpr. But declaring the closure constexpr does not seem to be enough when passing it to make_array_okHarness
@Harness See dupe?: Why explicitly capturing constexpr variable doesn't work while not capturing it worksSiclari
Sorry, I don't understand the provided answer in the linked post. It explains why the function argument cannot be used as a constant expression but here, it's not a function argument but a capture inside a constexpr closure. Comments in the linked post are rising this point but, IMHO, the question is not settled.Harness
@Harness Basically, making the lambda constexpr by explicitly writing constexpr as you did in size_capt doesn't guarantee that it will be called at compile time just like it is not guaranteed that a constexpr function will be called at compile time.Siclari
Indeed and the constexpr in the lambda declaration is meaningless in this issue. I'm more puzzled by (1) the difference between captureless and capturing, and by (2) why the example with the constexpr closure does not work. I'm updating my snippet to remove the noise added by extraneous constexpr.Harness
Looking closely to @BrianBi comment in stackoverflow.com/a/78094951, I'm eventually understanding the following: initializing the capture is as it was made from some constructor with an argument. Yet, a function argument (at least in a non-consteval function), inside an expression, does make the expression a non-constant one. Thus the initialized capture cannot be used either to produce a constant expression. Am I right? But how does it apply to constexpr closure initialisation?Harness
@Harness When you do the catpure, the lambda is not empty. See Is lvalue to rvalue conversion not applied on empty class object passed by value. I think this is what is preventing the code with size_capt to work.Siclari

© 2022 - 2024 — McMap. All rights reserved.