Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 &>);
like image 400
HolyBlackCat Avatar asked Oct 26 '25 05:10

HolyBlackCat


2 Answers

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.

like image 200
Jan Schultke Avatar answered Oct 27 '25 18:10

Jan Schultke


CWG2369 defines the current order of constraint checking. It says that you check associated constraints before you start substituting template parameters into the function signature.

CWG2770 points out that the resolution of CWG2369 doesn't fully make sense. A trailing requires-clause can reference parameters, so at least in that case the parameter types must be substituted into before the constraints can be checked. The proposed direction there (not yet worded) is that when checking constraints, we substitute on-demand into any function parameters that are referenced from those constraints.

In any case, the intended behaviour is that if std::signed_integral<T> is not satisfied, then deduction fails and substitution into the return type is not performed. I believe what you're seeing in GCC is a change in behaviour after CWG2369 was implemented.

Clang has not implemented CWG2369. The reasons for this are explained in P3606R0. So, like you say, perhaps they implemented a workaround in libc++ for that reason.

like image 30
Brian Bi Avatar answered Oct 27 '25 18:10

Brian Bi



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!