When satisfation of SFINAE condition depends on the function being declared, is that condition guaranteed to be false?

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{});
}

  • 3

    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/13o1ojafd

    – 




  • 1

    somewhat related stackoverflow.com/q/73308975/4117728

    – 

  • Did you intentionally ommit the not in the requires 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?

    – 




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

Leave a Comment