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();
}
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?
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:
std::array<int, callable()>
compiles, callable()
must be known at compile time,callable
is a constexpr
callable (e.g. a constepxr
function or the constexpr
operator()
of a lambda);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 :DI 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 constexpr
ness, 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.
... 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
.