c++language-lawyerc++-concepts

Differences in constraint validation order between compilers?


Consider the following code: https://gcc.godbolt.org/z/9qj15faEz

#include <concepts>
#include <string>
#include <type_traits>

template <std::signed_integral T>
std::make_unsigned_t<T> to_unsigned(T t)
{
    return t;
}

template <typename T>
concept CanMakeUnsigned = requires(const T &t){to_unsigned(t);};
static_assert(!CanMakeUnsigned<std::string>);

This compiles in GCC 11 and newer and in Clang with libc++. But it causes a hard error (in make_unsigned_t) in MSVC, Clang with libstdc++, and GCC 10 and older.

Which compiler is correct? Is this a legal code or not?

It seems that libc++ has made std::make_unsigned_t SFINAE-friendly (which I'm not sure is conforming), but the rest of the behavior is a mystery to me.

Moreover, I figured I could reproduce this with my own types, but all compilers accept the following for some reason: https://gcc.godbolt.org/z/zz3Kx1xxe

template <typename T> concept C = sizeof(T) == 42;

template <typename T> struct A
{
    T *x;
};

template <C T>
A<T> blah() {return {};}

template <typename T>
concept CanBlah = requires{blah<T>();};
static_assert(!CanBlah<int &>);

Solution

  • tl;dr the wording is defective and unclear. Who knows what's right and wrong?

    It seems that libc++ has made std::make_unsigned_t SFINAE-friendly (which I'm not sure is conforming), but the rest of the behavior is a mystery to me.

    It is not conforming. [tab:meta.trans.sign] uses Mandates specifications, making it non-SFINAE-friendly.

    See also [structure.specifications]:

    Descriptions of function semantics contain the following elements (as appropriate):

    • Mandates: the conditions that, if not met, render the program ill-formed.

    Also notice that Mandates and Constraints are only defined for function semantics in this paragraph, and iirc this has been pointed out as a LWG defect.

    Which compiler is correct? Is this a legal code or not?

    Who knows? To my understanding, this is a long-standing LWG defect. [structure.specifications] doesn't clarify when Mandates takes effect (i.e. does it only apply after instantiations?). In practice, Mandates is synonymous with "static_assert in the template", but that doesn't match the wording. If we take the definition of Mandates at face value, then std::make_unsigned_t<T> is ill-formed for non-integer T, even if uninstantiated.

    Discussion on LWG4160 discovered a similar source of confusion: the library states that the program is ill-formed in a number of places, but in practice, you only get a compiler error upon instantiation (and the standard should probably say that).

    Anyhow, the compiler divergence you're seeing is caused by compiler disagreement over when an instantiation is needed. The standard isn't very clear on when that's the case ([temp.inst] paragraph 2):

    Unless a class template specialization is a declared specialization, the class template specialization is implicitly instantiated when the specialization is referenced in a context that requires a completely-defined object type or when the completeness of the class type affects the semantics of the program.

    It's not so easy to figure out whether that's the case for std::make_unsigned_t<T> to_unsigned(T t). std::make_unsigned_t is an alias for std::make_unsigned<T>::type, and it's necessary to instantiate make_unsigned<T> to understand whether ::type exists and is a type. However, if you wrap it in a type alias and the constraints of to_unsigned aren't satisfied, does it even matter? "affects the semantics of the program" leaves a lot of room for interpretation.

    I suppose GCC flipped its behavior between GCC 10 and GCC 11, and that change seems to be better for user experience.

    Conclusion

    You have stumbled upon a deeply defective area of both library and core wording. If we read the wording very strictly, then yes, std::make_unsigned_t should make the program ill-formed. However, that's not what compilers do in practice, and it's likely not what CWG/LWG want them to do.

    As a workaround, you could write it as follows:

    template <std::signed_integral T>
    auto to_unsigned(T t)
    {
        return std::make_unsigned_t<T>(t);
    }
    

    Other example

    Moreover, I figured I could reproduce this with my own types, but all compilers accept the following for some reason: https://gcc.godbolt.org/z/zz3Kx1xxe

    Your second example has more clearly specified semantics. The program would only be ill-formed once A<T> is instantiated, due to the T* x member, where T = int& (pointers to references are not allowed). Templates are only instantiated when necessary, and an instantiation of A<T> is not needed if C<T> is false.

    That is because A<T> is a class template; it's obviously a valid return type, no ::type trickery.