Search code examples
c++constant-expressioncallable-objectconstexpr-function

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?


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?


Solution

  • 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.