-
Notifications
You must be signed in to change notification settings - Fork 12.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Incorrect if constexpr
evaluation in nested generic lambda
#58872
Comments
@llvm/issue-subscribers-clang-frontend |
If the condition is not value-dependent then the discarded statement should not be instantiated stmt.if p2. I believe Maybe @erichkeane might have a better idea |
I think the problem is that It is only when the call to the lambda happens ( |
I'm not a language lawyer, so forgive me if I get this completely wrong :) But the linked standardese above talks about "value-dependent" expressions. The expressions here only involves types, hence this is never value-dependent? Edit: reading some more about it, value-dependent doesn't mean what I think it means. Still, it seems to me the body of the inner lambda, which is a template, shouldn't be instanciated until called, regardless of whether it refers to |
See here: https://eel.is/c++draft/temp.dep.constexpr#2.2
As far as the instantiation: The BODY of the lambda gets partially instantiated as a part of instantiating |
I have refactored the original code to not use explicit template parameters in the inner lambda, and the issue still occurs. This confirms this isn't specific to lambdas with explicit template parameters, and applies to any nested generic lambda. Modified code without explicit template parameters in the lambda.#include <type_traits>
// Generic type holder, for types that cannot be instanciated.
template<typename T>
struct type_holder {};
// Generic type to create a function with a given signature.
template<typename T>
struct foo;
template<typename R, typename ... Args>
struct foo<R(Args...)> {
static R bar(Args...) {}
};
// Generic type trait to extract the return type of a function signature.
template<typename T>
struct return_type_of_t;
template<typename R, typename ... Args>
struct return_type_of_t<type_holder<R(Args...)>> {
using type = R;
};
template<typename T>
using return_type_of = typename return_type_of_t<T>::type;
// Offending code.
template<typename T, typename ... IArgs>
auto test(IArgs... inputs) {
[&](auto holder, auto... values) {
// This always works.
foo<T>::bar(values...);
// This does not always work.
using R = return_type_of<decltype(holder)>;
if constexpr (std::is_same_v<R, void>) {
if constexpr (sizeof...(values) > 0) {
foo<T>::bar(values...);
} else {
foo<T>::bar();
}
} else {
int return_value = 0;
if constexpr (sizeof...(values) > 0) {
return_value = foo<T>::bar(values...);
} else {
return_value = foo<T>::bar();
}
}
}(type_holder<T>{}, inputs...);
}
int main() {
test<void()>(); // complains on line 35 'assigning to 'int' from incompatible type 'void''
test<void(int)>(1); // complains on line 28 'too few arguments to function call, expected 1, have 0'
test<int()>(); // works!
test<int(int)>(1); // complains on line 28 'too few arguments to function call, expected 1, have 0'
return 0;
} That being said, I don't think the problem is about Modified code to not use a class dependent on T inside the constexpr if.#include <type_traits>
// Generic type holder, for types that cannot be instanciated.
template<typename T>
struct type_holder {};
// Set of non-template free functions to call later.
void free_bar() {}
void free_bar_with_param(int) {}
int free_bar_with_return() { return 0; }
int free_bar_with_return_and_param(int) { return 0; }
// Generic type trait to extract the return type of a function signature.
template<typename T>
struct return_type_of_t;
template<typename R, typename ... Args>
struct return_type_of_t<type_holder<R(Args...)>> {
using type = R;
};
template<typename T>
using return_type_of = typename return_type_of_t<T>::type;
// This now compiles fine
template<typename T, typename ... IArgs>
auto test(IArgs... inputs) {
[&](auto holder, auto... values) {
using R = return_type_of<decltype(holder)>;
if constexpr (std::is_same_v<R, void>) {
if constexpr (sizeof...(values) > 0) {
free_bar_with_param(values...);
} else {
free_bar();
}
} else {
int return_value = 0;
if constexpr (sizeof...(values) > 0) {
return_value = free_bar_with_return_and_param(values...);
} else {
return_value = free_bar_with_return();
}
}
}(type_holder<T>{}, inputs...);
}
int main() {
test<void()>(); // works!
test<void(int)>(1); // works!
test<int()>(); // works!
test<int(int)>(1); // works!
return 0;
} This suggests to me that the |
if constexpr
evaluation in nested generic lambda with explicit template parametersif constexpr
evaluation in nested generic lambda
I'll also point out that the standardese for
It does not say that "if the condition is value-dependent, the discarded substatement is instantiated." That would defeat the whole purpose of the feature. Here is a counter-example. Inside #include <type_traits>
template<typename T>
struct foo {
template<typename U>
U bar() {
if constexpr (std::is_same_v<U, void>) {
// do nothing
} else {
return sizeof(T);
}
}
};
int main() {
foo<void> f1; // instantiates 'foo' but not 'bar'. OK.
f1.bar<void>(); // instantiates 'bar'; takes the 'if' branch, OK.
f1.bar<int>(); // instantiates 'bar'; takes the 'else' branch, does no compile.
return 0;
} This works as I would expect. However, doing the same thing with a generic lambda nested inside a function, even when the lambda is never instantiated, generates an error in clang (but not GCC/MSVC): #include <type_traits>
template<typename T>
void foo() {
auto bar = []<typename U>() {
if constexpr (std::is_same_v<U, void>) {
// do nothing
} else {
return sizeof(T);
}
};
}
int main() {
foo<void>(); // error, even though `decltype(bar)::operator()<U>` is never instantiated.
return 0;
} Is there something special about generic lambdas nested inside a template function, that they should get more aggressively instantiated? |
I attempted to look at the code of llvm-project/clang/lib/Sema/TreeTransform.h Lines 7544 to 7565 in 1a6d770
|
Right, the problem is the definition of the lambda being in the definition of 'test', the problem is only relevant to you because the lambda itself is generic in some way.
I don't see what you mean? If they don't depend on T, they are going to not fail the instantiation with template parameter T .
THAT phrase is an exception to the normal instantiation rules. So it doesn't have to say that, it is already true. It doesn't defeat the purpose of the feature, it is in fact part of how the feature is designed.
The difference HERE is that function templates, while part of their class-template, don't have their body instantiated until they are called. Else the use of members wouldn't work right in the body of hte function, this is to be expected.
The 'special' thing about lambdas is that their definitions exist inside of a function. Thus, we need to instantiate them as we instantiate the containing function template. However, with generic lambdas, we end up creating the 'new' generic version for the function template instantiation.
Yep, this looks perfectly correct to me. I don't see a bug here. This is simply a case of Clang diagnosing an IFNDR that the other compilers don't. |
I don't see any defect here still, this is working exactly how template instantiation and if-constexpr is supposed to work, so closing as 'not a defect'. |
The standard says that lambdas are function objects, implemented as a closure type with a function call operator. The body of the lambda is therefore that of a function, I don't see anything in the standard that says this body should be instanciated differently than other function templates.
I think this is where our understanding differ. Why do you need to instanciate it? To creat the object |
In fact:
Could this be reopened please? |
My understanding is that historically our belief was that the first part of this doesn't apply to a lambda body, as they're special in that regard. That said, that note is one I've not noticed before, and I'd very like to make sure we get this right, so I'm hopeful one of my core experts can come along and clarify that we're wrong here. @hubert-reinterpretcast is our standards expert here. As far as implementation, we'd have to stop instantiating-into the body of a lambda during normal tree-transform and delay said instantiation like we do with a member function. |
That note is particularly confusing, because we AREN'T implicitly instantiating a the lambda, we're instantiating the containing definition, of which it is a a part. By my reading that is not allowing us to do the full instantiation here until we hit the call operator. BUT, that makes it somewhat of a non-sequitur here... |
My guess is that any such historical belief stemmed from the old determination of lambda captures, which involved determining odr-use. This was changed between C++17 and C++20. |
Confirmed: See P0588R1 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0588r1.html), adopted as DR: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4709.pdf |
Thank you for the pointers! This issue is then a duplicate of #44851, so it should probably stay closed. Out of curiosity, is the status of this DR tracked somewhere? I may be able to help implementing it if no one is working on it yet. |
Thanks Hubert! That's exactly what I was looking for. DR status is tracked here : https://clang.llvm.org/cxx_dr_status.html So it would be tracked there as 1632 and 1913. From the looks of it, implementation will likely quickly get into a "thar be dragons" part of the compiler. We need to suppress instantiation of the body of dependent lambdas (the easy part), but getInstantiationArgs likely needs updating for it (which is a function that had some serious problems) to properly handle this and other cases. Ive been messing with it recently for other issues, so perhaps this is another I have to make dependent on that. |
Note: I did a few hrs of poking at this, and got myself closer to 'fixed' than I thought with a lot less effort than i thought. Unfortunately, this is a fairly significant breaking change, so at one poitn we'll have to update a ton of tests: https://reviews.llvm.org/D138148 We ALSO have a ton of crashes. that come out of this, but I don't know if that is just me missing a hack somewhere, or putting the lambda-body-transformation work in teh wrong place. @cschreib : you had interest in helping on this, so feel free to poke at this further and update that review if you make progress. |
Sorry, I thought I had replied to that last comment. I had a look at the proposed changes back then, and I think it flies way over my head. I could probably have helped if it was an easy change, but this isn't. |
Ah, no worries! I'd not seen anything, but got distracted with a ton of other things anyway, hopefully I'll have time to poke this again. |
This comment was marked as duplicate.
This comment was marked as duplicate.
Is this the same issue, or should I open a new one (https://compiler-explorer.com/z/oh4M5xKW3): struct t {
void f();
static void g(auto x) {
[](auto obj) {
if constexpr (requires { obj.f(); }) obj.f();
else f(obj);
}(x);
}
};
|
The test code below does not compile (in C++20 mode) with any version of clang I could test in Compiler Explorer (up to and including clang 15):
Test cases:
test<void()>()
generates an error inside anif constexpr
branch that should not be entered.std::is_same_v<R, void>
should be true, but it took (or at least, also tried to compile) the false branch.test<void(int)>(1)
. There it took the correct branch for the return type, but then took the wrong branch for the arguments.sizeof...(Args) > 0
should be true, but it took (or at least, also tried to compile) the false branch.test<int()>()
works fine, for some reason (it is the case that corresponds to theelse
branch for allif constexpr
checks).test<int(int)>(1)
fails again and takes the wrong branch for the return value and the arguments.Extra information:
foo<R(Args...)>
instead offoo<T>
, which should be equivalent. Weirdly enough, as this seems orthogonal to theif constexpr
issue.-std=c++2a
).-std=c++2a
) and above.The text was updated successfully, but these errors were encountered: