Consider this code:
#include <type_traits>
struct A {};
struct B
{
template <typename T, std::enable_if_t<!std::is_constructible_v<B, T &&>, std::nullptr_t> = nullptr>
B(T &&) {}
};
int main()
{
B b(A{});
}
Both Clang, GCC, and MSVC accept it.
Is this code valid, or should the compilers reject it?
Interestingly, if I use requires
instead of enable_if_t
, GCC and Clang start rejecting it, while MSVC still accepts. The error boils down to satisfaction of constraint depends on itself
.
But shouldn’t the same thing cause an error in the first code? Or is this undefined?
#include <type_traits>
struct A {};
struct B
{
template <typename T>
requires(!std::is_constructible_v<B, T &&>)
B(T &&) {}
};
int main()
{
B b(A{});
}
is_constructible_v<T...>
is an inline constexpr bool
.
B b(A{});
does an overload resolution to pick a viable constructor.
When checking if template <typename T, std::enable_if_t<!std::is_constructible_v<B, T &&>, std::nullptr_t> = nullptr> B(T &&)
is viable, std::is_constructible_v<B, T &&>
is evaluated, where T
is deduced as A
.
To evaluate std::is_constructible_v<B, A &&>
, the variable template is instantiated and it’s initializer is evaluated. Overload resolution is done again for B t(declval<A&&>());
([meta.unary.prop]p9).
This again tries the same constructor template and has to check for the value of std::is_constructible_v<B, A &&>
. However, this refers to the same variable std::is_constructible_v<B, A &&>
from beforehand. Since the initialization isn’t finished, the variable is not in its lifetime ([basic.life]p(1.2)), so reading it is not a constant expression, so std::is_constructible_v<B, A &&>
isn’t a constant expression in this case. This is a SFINAE-able error, so the constructor isn’t viable.
This means that the original std::is_constructible_v<B, A &&>
is now initialized to false
, making your original constructor viable.
A similar thing happens if you have std::is_constructible<B, T &&>::value
, except std::is_constructible<B, A &&>
is an incomplete type the second time around and ::value
is the SFINAE error.
I think this is still IF-NDR. The second overload resolution depends on the value of std::is_constructible_v<B, A &&>
, which would have a different result depending on where it is evaluated (true
or false
instead of “not yet in lifetime”). And [temp.point]p7:
If two different points of instantiation give a template specialization different meanings according to the one-definition rule, the program is ill-formed, no diagnostic required.
Though I could not find how points of instantiation are defined for variable templates (or the initializer for a variable template).
I suspect you run foul of [temp.point]p7. If you replace the template with the built-in
__is_constructible
(which is evaluated every time and not cached like templates), you run into an infinite template instantiation loop: gcc.godbolt.org/z/13o1ojafdsomewhat related stackoverflow.com/q/73308975/4117728
Did you intentionally ommit the
not
in therequires
snippet, or was it a typo? The test conditions are opposite between the snippets.@Red.Wave Yep, it was a typo, thanks.
@Artyer Can you elaborate on this? Where are the two instantiations, and of which template?
Show 1 more comment