Is it legal to use std::declval in lambda in unevaluated contexts?
Asked Answered
S

1

8

Code as below or on godbolt compiles with gcc and MSVC but fails with clang. I couldn't find if/where it is forbidden in the standard. In my opinion it should be supported.

So who is correct on this, clang or gcc/MSVC?

#include <type_traits>

void foo() {
    static_assert(decltype([_=std::declval<int>()]() consteval noexcept { // clang error: declval() must not be used
        if constexpr (std::is_integral<decltype(_)>::value) {
            return std::bool_constant<true>();
        } else {
            return std::bool_constant<false>();
        }
    }())::value);
}

The example could be expanded into 3 cases as below or on godbolt:

  1. as lambda call argument: OK with clang/gcc/MSVC
  2. as lambda capture: OK with gcc/MSVC, error with clang
  3. in lambda body: error with clang/gcc/MSVC

So it seems clear that it is not legal in lambda body but legal outside as caller argument. It is not clear if it is allowed in capture list.

#include <type_traits>

auto foo_lambda_argument() {
    return decltype([](auto _) noexcept {
        return std::bool_constant<std::is_integral<decltype(_)>::value>();
    }(std::declval<int>()))::value; // OK with clang/gcc/MSVC
}

auto foo_capture_list() {
    return decltype([_=std::declval<int>()]() noexcept { // OK with gcc/MSVC; clang error: declval() must not be used
        return std::bool_constant<std::is_integral<decltype(_)>::value>();
    }())::value;
}

auto foo_lambda_body() {
    return decltype([]() noexcept {
        auto _=std::declval<int>(); // clang/gcc/MSVC error
        return std::bool_constant<std::is_integral<decltype(_)>::value>();
    }())::value;
}
Smoothspoken answered 18/11, 2022 at 8:39 Comment(9)
maybe add the language-lawyer tagEyestalk
Looks like that is because std::declval() is not a constexpr function. See: en.cppreference.com/w/cpp/utility/declvalDemaggio
I do not think static_assert is a unevaluated context. The expression in the static_assert is evaluated at compile time. std::declval() cannot be used in the evaliated context - thus an error.Demaggio
remove static_assert and it's the same error with clang: godbolt.org/z/MGzhMd19MSmoothspoken
If you remove static_assert, you get lambda. Lambda is a evaluated context, no?Demaggio
The whole point is about using std::declval() in lambda in unevaluated contexts and std::declval() could only be used in unevaluated contexts. It is actually doesn't matter if std::decval() is constexpr or not: godbolt.org/z/q6hfKEexfSmoothspoken
It seems dubious to have code store the result of declval, even in an unevaluated context. The specific error you're running into is there in the library code to stop people from trying to actually call the function. GCC gives me the same error if I try to put if (std::declval<int>() == 0) into an unevaluated lambda.Comply
IMO if (std::decval<int>()==0) is obviously a hard error while the example in the post is not. I just want to use the simplest example to show what the problem might be even though it's a bit silly. But I think it is still a valid use case. The reason why I come up with this strange thing is that MSVC has some other compiler bug if _ is lambda parameter and we are developing a cross-platform software.Smoothspoken
The if case is using the result of calling the function, just like how your code is using it to initialize a capture. I don't see how they're different conceptually. If that doesn't float your boat, though, auto x = std::declval<int>(); is the same thing, and that's almost exactly what your capture is doing behind the scenes. Either way, it's up to the standard to handle all of these.Comply
F
3

I am not completely sure how this is intended to work, but here is my attempt at a solution:

According to [intro.execution]/3.3 the initializer of an init-capture is an immediate subexpression of a lambda expression. However none of the listed items make the expressions in the lambda's body subexpressions.

Unevaluated operands are non-potentially-evaluated expressions. But only their subexpressions are also non-potentially-evaluated expressions. (see [basic.def.odr]/2)

A function is odr-used if it is named by a potentially-evaluated expression or in some special cases that are not relevant here.

So std::declval should be fine in the init-capture initializer as in your point 2.

It is also ok in point 1, because the function arguments are subexpressions of the function call expression making up the whole unevaluated operand.

Point 3 is not ok, because the expressions in the body of the lambda are potentially-evaluated even if the lambda expression appears in an unevaluated operand.


One concern I have with the reasoning above is that [exp.prim.lambda.capture]/6 specified an init-capture to behave as if declaring a variable with the given intializer and then capturing it. I am not sure where this fits into the above (the declaration couldn't be a subexpression) and whether it would count as odr-use.

Flyspeck answered 19/11, 2022 at 3:42 Comment(1)
Hmm, that does make sense for the difference to be an immediate context or not.Comply

© 2022 - 2024 — McMap. All rights reserved.