Author: Botond István Horváth Date: 2024-03-06T15:03:21+01:00 New Revision: 63afcbbeeaf68751b57acc1f2911afee767d86bb
URL: https://github.com/llvm/llvm-project/commit/63afcbbeeaf68751b57acc1f2911afee767d86bb DIFF: https://github.com/llvm/llvm-project/commit/63afcbbeeaf68751b57acc1f2911afee767d86bb.diff LOG: [clang][Sema] Bugfix for choosing the more specialized overload (#83279) There was a bug in Clang where it couldn't choose which overload candidate was more specialized if it was comparing a member-function to a non-member function. Previously, this was detected as an ambiguity, now Clang chooses correctly. This patch fixes the bug by fully implementing CWG2445 and moving the template transformation described in `[temp.func.order]` paragraph 3 from `isAtLeastAsSpecializedAs()` to `Sema::getMoreSpecializedTemplate()` so we have the transformed parameter list during the whole comparison. Also, to be able to add the correct type for the implicit object parameter `Sema::getMoreSpecializedTemplate()` has new parameters for the object type. Fixes #74494, fixes #82509 Added: Modified: clang/docs/ReleaseNotes.rst clang/include/clang/Sema/Sema.h clang/lib/Sema/SemaOverload.cpp clang/lib/Sema/SemaTemplateDeduction.cpp clang/test/CXX/drs/dr24xx.cpp clang/test/SemaCXX/overload-template.cpp clang/test/SemaCXX/overloaded-operator.cpp clang/www/cxx_dr_status.html Removed: ################################################################################ diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst index 0ff4a93b15ea8f..942820a5268576 100644 --- a/clang/docs/ReleaseNotes.rst +++ b/clang/docs/ReleaseNotes.rst @@ -319,6 +319,12 @@ Bug Fixes to C++ Support Fixes (#GH80630) - Fix a crash when an explicit template argument list is used with a name for which lookup finds a non-template function and a dependent using declarator. +- Fix a bug where overload resolution falsely reported an ambiguity when it was comparing + a member-function against a non member function or a member-function with an + explicit object parameter against a member function with no explicit object parameter + when one of the function had more specialized templates. + Fixes (`#82509 <https://github.com/llvm/llvm-project/issues/82509>`_) + and (`#74494 <https://github.com/llvm/llvm-project/issues/74494>`_) Bug Fixes to AST Handling ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index f3d3a57104ee07..2d949f3fc9a718 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -9952,7 +9952,8 @@ class Sema final { FunctionTemplateDecl *getMoreSpecializedTemplate( FunctionTemplateDecl *FT1, FunctionTemplateDecl *FT2, SourceLocation Loc, TemplatePartialOrderingContext TPOC, unsigned NumCallArguments1, - unsigned NumCallArguments2, bool Reversed = false); + QualType RawObj1Ty = {}, QualType RawObj2Ty = {}, bool Reversed = false); + UnresolvedSetIterator getMostSpecialized(UnresolvedSetIterator SBegin, UnresolvedSetIterator SEnd, TemplateSpecCandidateSet &FailedCandidates, diff --git a/clang/lib/Sema/SemaOverload.cpp b/clang/lib/Sema/SemaOverload.cpp index a03f3eae5478eb..b0c693f078efe2 100644 --- a/clang/lib/Sema/SemaOverload.cpp +++ b/clang/lib/Sema/SemaOverload.cpp @@ -10571,14 +10571,23 @@ bool clang::isBetterOverloadCandidate( // according to the partial ordering rules described in 14.5.5.2, or, // if not that, if (Cand1IsSpecialization && Cand2IsSpecialization) { + const auto *Obj1Context = + dyn_cast<CXXRecordDecl>(Cand1.FoundDecl->getDeclContext()); + const auto *Obj2Context = + dyn_cast<CXXRecordDecl>(Cand2.FoundDecl->getDeclContext()); if (FunctionTemplateDecl *BetterTemplate = S.getMoreSpecializedTemplate( Cand1.Function->getPrimaryTemplate(), Cand2.Function->getPrimaryTemplate(), Loc, isa<CXXConversionDecl>(Cand1.Function) ? TPOC_Conversion : TPOC_Call, - Cand1.ExplicitCallArguments, Cand2.ExplicitCallArguments, - Cand1.isReversed() ^ Cand2.isReversed())) + Cand1.ExplicitCallArguments, + Obj1Context ? QualType(Obj1Context->getTypeForDecl(), 0) + : QualType{}, + Obj2Context ? QualType(Obj2Context->getTypeForDecl(), 0) + : QualType{}, + Cand1.isReversed() ^ Cand2.isReversed())) { return BetterTemplate == Cand1.Function->getPrimaryTemplate(); + } } // -— F1 and F2 are non-template functions with the same diff --git a/clang/lib/Sema/SemaTemplateDeduction.cpp b/clang/lib/Sema/SemaTemplateDeduction.cpp index 563491f76f5478..65f7fa15b20dd7 100644 --- a/clang/lib/Sema/SemaTemplateDeduction.cpp +++ b/clang/lib/Sema/SemaTemplateDeduction.cpp @@ -5333,38 +5333,38 @@ bool Sema::CheckIfFunctionSpecializationIsImmediate(FunctionDecl *FD, return false; } -/// If this is a non-static member function, -static void -AddImplicitObjectParameterType(ASTContext &Context, - CXXMethodDecl *Method, - SmallVectorImpl<QualType> &ArgTypes) { - // C++11 [temp.func.order]p3: - // [...] The new parameter is of type "reference to cv A," where cv are - // the cv-qualifiers of the function template (if any) and A is - // the class of which the function template is a member. +static QualType GetImplicitObjectParameterType(ASTContext &Context, + const CXXMethodDecl *Method, + QualType RawType, + bool IsOtherRvr) { + // C++20 [temp.func.order]p3.1, p3.2: + // - The type X(M) is "rvalue reference to cv A" if the optional + // ref-qualifier of M is && or if M has no ref-qualifier and the + // positionally-corresponding parameter of the other transformed template + // has rvalue reference type; if this determination depends recursively + // upon whether X(M) is an rvalue reference type, it is not considered to + // have rvalue reference type. // - // The standard doesn't say explicitly, but we pick the appropriate kind of - // reference type based on [over.match.funcs]p4. - assert(Method && Method->isImplicitObjectMemberFunction() && - "expected an implicit objet function"); - QualType ArgTy = Context.getTypeDeclType(Method->getParent()); - ArgTy = Context.getQualifiedType(ArgTy, Method->getMethodQualifiers()); - if (Method->getRefQualifier() == RQ_RValue) - ArgTy = Context.getRValueReferenceType(ArgTy); - else - ArgTy = Context.getLValueReferenceType(ArgTy); - ArgTypes.push_back(ArgTy); + // - Otherwise, X(M) is "lvalue reference to cv A". + assert(Method && !Method->isExplicitObjectMemberFunction() && + "expected a member function with no explicit object parameter"); + + RawType = Context.getQualifiedType(RawType, Method->getMethodQualifiers()); + if (Method->getRefQualifier() == RQ_RValue || + (IsOtherRvr && Method->getRefQualifier() == RQ_None)) + return Context.getRValueReferenceType(RawType); + return Context.getLValueReferenceType(RawType); } /// Determine whether the function template \p FT1 is at least as /// specialized as \p FT2. -static bool isAtLeastAsSpecializedAs(Sema &S, - SourceLocation Loc, - FunctionTemplateDecl *FT1, - FunctionTemplateDecl *FT2, +static bool isAtLeastAsSpecializedAs(Sema &S, SourceLocation Loc, + const FunctionTemplateDecl *FT1, + const FunctionTemplateDecl *FT2, TemplatePartialOrderingContext TPOC, - unsigned NumCallArguments1, - bool Reversed) { + bool Reversed, + const SmallVector<QualType> &Args1, + const SmallVector<QualType> &Args2) { assert(!Reversed || TPOC == TPOC_Call); FunctionDecl *FD1 = FT1->getTemplatedDecl(); @@ -5381,66 +5381,8 @@ static bool isAtLeastAsSpecializedAs(Sema &S, // The types used to determine the ordering depend on the context in which // the partial ordering is done: TemplateDeductionInfo Info(Loc); - SmallVector<QualType, 4> Args2; switch (TPOC) { - case TPOC_Call: { - // - In the context of a function call, the function parameter types are - // used. - CXXMethodDecl *Method1 = dyn_cast<CXXMethodDecl>(FD1); - CXXMethodDecl *Method2 = dyn_cast<CXXMethodDecl>(FD2); - - // C++11 [temp.func.order]p3: - // [...] If only one of the function templates is a non-static - // member, that function template is considered to have a new - // first parameter inserted in its function parameter list. The - // new parameter is of type "reference to cv A," where cv are - // the cv-qualifiers of the function template (if any) and A is - // the class of which the function template is a member. - // - // Note that we interpret this to mean "if one of the function - // templates is a non-static member and the other is a non-member"; - // otherwise, the ordering rules for static functions against non-static - // functions don't make any sense. - // - // C++98/03 doesn't have this provision but we've extended DR532 to cover - // it as wording was broken prior to it. - SmallVector<QualType, 4> Args1; - - unsigned NumComparedArguments = NumCallArguments1; - - if (!Method2 && Method1 && Method1->isImplicitObjectMemberFunction()) { - // Compare 'this' from Method1 against first parameter from Method2. - AddImplicitObjectParameterType(S.Context, Method1, Args1); - ++NumComparedArguments; - } else if (!Method1 && Method2 && - Method2->isImplicitObjectMemberFunction()) { - // Compare 'this' from Method2 against first parameter from Method1. - AddImplicitObjectParameterType(S.Context, Method2, Args2); - } else if (Method1 && Method2 && Reversed && - Method1->isImplicitObjectMemberFunction() && - Method2->isImplicitObjectMemberFunction()) { - // Compare 'this' from Method1 against second parameter from Method2 - // and 'this' from Method2 against second parameter from Method1. - AddImplicitObjectParameterType(S.Context, Method1, Args1); - AddImplicitObjectParameterType(S.Context, Method2, Args2); - ++NumComparedArguments; - } - - Args1.insert(Args1.end(), Proto1->param_type_begin(), - Proto1->param_type_end()); - Args2.insert(Args2.end(), Proto2->param_type_begin(), - Proto2->param_type_end()); - - // C++ [temp.func.order]p5: - // The presence of unused ellipsis and default arguments has no effect on - // the partial ordering of function templates. - if (Args1.size() > NumComparedArguments) - Args1.resize(NumComparedArguments); - if (Args2.size() > NumComparedArguments) - Args2.resize(NumComparedArguments); - if (Reversed) - std::reverse(Args2.begin(), Args2.end()); - + case TPOC_Call: if (DeduceTemplateArguments(S, TemplateParams, Args2.data(), Args2.size(), Args1.data(), Args1.size(), Info, Deduced, TDF_None, /*PartialOrdering=*/true) != @@ -5448,7 +5390,6 @@ static bool isAtLeastAsSpecializedAs(Sema &S, return false; break; - } case TPOC_Conversion: // - In the context of a call to a conversion operator, the return types @@ -5536,8 +5477,13 @@ static bool isAtLeastAsSpecializedAs(Sema &S, /// \param NumCallArguments1 The number of arguments in the call to FT1, used /// only when \c TPOC is \c TPOC_Call. /// -/// \param NumCallArguments2 The number of arguments in the call to FT2, used -/// only when \c TPOC is \c TPOC_Call. +/// \param RawObj1Ty The type of the object parameter of FT1 if a member +/// function only used if \c TPOC is \c TPOC_Call and FT1 is a Function +/// template from a member function +/// +/// \param RawObj2Ty The type of the object parameter of FT2 if a member +/// function only used if \c TPOC is \c TPOC_Call and FT2 is a Function +/// template from a member function /// /// \param Reversed If \c true, exactly one of FT1 and FT2 is an overload /// candidate with a reversed parameter order. In this case, the corresponding @@ -5548,13 +5494,76 @@ static bool isAtLeastAsSpecializedAs(Sema &S, FunctionTemplateDecl *Sema::getMoreSpecializedTemplate( FunctionTemplateDecl *FT1, FunctionTemplateDecl *FT2, SourceLocation Loc, TemplatePartialOrderingContext TPOC, unsigned NumCallArguments1, - unsigned NumCallArguments2, bool Reversed) { + QualType RawObj1Ty, QualType RawObj2Ty, bool Reversed) { + SmallVector<QualType> Args1; + SmallVector<QualType> Args2; + const FunctionDecl *FD1 = FT1->getTemplatedDecl(); + const FunctionDecl *FD2 = FT2->getTemplatedDecl(); + bool ShouldConvert1 = false; + bool ShouldConvert2 = false; + QualType Obj1Ty; + QualType Obj2Ty; + if (TPOC == TPOC_Call) { + const FunctionProtoType *Proto1 = + FD1->getType()->getAs<FunctionProtoType>(); + const FunctionProtoType *Proto2 = + FD2->getType()->getAs<FunctionProtoType>(); + + // - In the context of a function call, the function parameter types are + // used. + const CXXMethodDecl *Method1 = dyn_cast<CXXMethodDecl>(FD1); + const CXXMethodDecl *Method2 = dyn_cast<CXXMethodDecl>(FD2); + // C++20 [temp.func.order]p3 + // [...] Each function template M that is a member function is + // considered to have a new first parameter of type + // X(M), described below, inserted in its function parameter list. + // + // Note that we interpret "that is a member function" as + // "that is a member function with no expicit object argument". + // Otherwise the ordering rules for methods with expicit objet arguments + // against anything else make no sense. + ShouldConvert1 = Method1 && !Method1->isExplicitObjectMemberFunction(); + ShouldConvert2 = Method2 && !Method2->isExplicitObjectMemberFunction(); + if (ShouldConvert1) { + bool IsRValRef2 = + ShouldConvert2 + ? Method2->getRefQualifier() == RQ_RValue + : Proto2->param_type_begin()[0]->isRValueReferenceType(); + // Compare 'this' from Method1 against first parameter from Method2. + Obj1Ty = GetImplicitObjectParameterType(this->Context, Method1, RawObj1Ty, + IsRValRef2); + Args1.push_back(Obj1Ty); + } + if (ShouldConvert2) { + bool IsRValRef1 = + ShouldConvert1 + ? Method1->getRefQualifier() == RQ_RValue + : Proto1->param_type_begin()[0]->isRValueReferenceType(); + // Compare 'this' from Method2 against first parameter from Method1. + Obj2Ty = GetImplicitObjectParameterType(this->Context, Method2, RawObj2Ty, + IsRValRef1); + Args2.push_back(Obj2Ty); + } + size_t NumComparedArguments = NumCallArguments1 + ShouldConvert1; + + Args1.insert(Args1.end(), Proto1->param_type_begin(), + Proto1->param_type_end()); + Args2.insert(Args2.end(), Proto2->param_type_begin(), + Proto2->param_type_end()); - bool Better1 = isAtLeastAsSpecializedAs(*this, Loc, FT1, FT2, TPOC, - NumCallArguments1, Reversed); - bool Better2 = isAtLeastAsSpecializedAs(*this, Loc, FT2, FT1, TPOC, - NumCallArguments2, Reversed); + // C++ [temp.func.order]p5: + // The presence of unused ellipsis and default arguments has no effect on + // the partial ordering of function templates. + Args1.resize(std::min(Args1.size(), NumComparedArguments)); + Args2.resize(std::min(Args2.size(), NumComparedArguments)); + if (Reversed) + std::reverse(Args2.begin(), Args2.end()); + } + bool Better1 = isAtLeastAsSpecializedAs(*this, Loc, FT1, FT2, TPOC, Reversed, + Args1, Args2); + bool Better2 = isAtLeastAsSpecializedAs(*this, Loc, FT2, FT1, TPOC, Reversed, + Args2, Args1); // C++ [temp.deduct.partial]p10: // F is more specialized than G if F is at least as specialized as G and G // is not at least as specialized as F. @@ -5568,12 +5577,28 @@ FunctionTemplateDecl *Sema::getMoreSpecializedTemplate( // ... and if G has a trailing function parameter pack for which F does not // have a corresponding parameter, and if F does not have a trailing // function parameter pack, then F is more specialized than G. - FunctionDecl *FD1 = FT1->getTemplatedDecl(); - FunctionDecl *FD2 = FT2->getTemplatedDecl(); - unsigned NumParams1 = FD1->getNumParams(); - unsigned NumParams2 = FD2->getNumParams(); - bool Variadic1 = NumParams1 && FD1->parameters().back()->isParameterPack(); - bool Variadic2 = NumParams2 && FD2->parameters().back()->isParameterPack(); + + SmallVector<QualType> Param1; + Param1.reserve(FD1->param_size() + ShouldConvert1); + if (ShouldConvert1) + Param1.push_back(Obj1Ty); + for (const auto &P : FD1->parameters()) + Param1.push_back(P->getType()); + + SmallVector<QualType> Param2; + Param2.reserve(FD2->param_size() + ShouldConvert2); + if (ShouldConvert2) + Param2.push_back(Obj2Ty); + for (const auto &P : FD2->parameters()) + Param2.push_back(P->getType()); + + unsigned NumParams1 = Param1.size(); + unsigned NumParams2 = Param2.size(); + + bool Variadic1 = + FD1->param_size() && FD1->parameters().back()->isParameterPack(); + bool Variadic2 = + FD2->param_size() && FD2->parameters().back()->isParameterPack(); if (Variadic1 != Variadic2) { if (Variadic1 && NumParams1 > NumParams2) return FT2; @@ -5584,8 +5609,8 @@ FunctionTemplateDecl *Sema::getMoreSpecializedTemplate( // This a speculative fix for CWG1432 (Similar to the fix for CWG1395) that // there is no wording or even resolution for this issue. for (int i = 0, e = std::min(NumParams1, NumParams2); i < e; ++i) { - QualType T1 = FD1->getParamDecl(i)->getType().getCanonicalType(); - QualType T2 = FD2->getParamDecl(i)->getType().getCanonicalType(); + QualType T1 = Param1[i].getCanonicalType(); + QualType T2 = Param2[i].getCanonicalType(); auto *TST1 = dyn_cast<TemplateSpecializationType>(T1); auto *TST2 = dyn_cast<TemplateSpecializationType>(T2); if (!TST1 || !TST2) @@ -5644,8 +5669,7 @@ FunctionTemplateDecl *Sema::getMoreSpecializedTemplate( // Any top-level cv-qualifiers modifying a parameter type are deleted when // forming the function type. for (unsigned i = 0; i < NumParams1; ++i) - if (!Context.hasSameUnqualifiedType(FD1->getParamDecl(i)->getType(), - FD2->getParamDecl(i)->getType())) + if (!Context.hasSameUnqualifiedType(Param1[i], Param2[i])) return nullptr; // C++20 [temp.func.order]p6.3: @@ -5733,8 +5757,8 @@ UnresolvedSetIterator Sema::getMostSpecialized( FunctionTemplateDecl *Challenger = cast<FunctionDecl>(*I)->getPrimaryTemplate(); assert(Challenger && "Not a function template specialization?"); - if (isSameTemplate(getMoreSpecializedTemplate(BestTemplate, Challenger, - Loc, TPOC_Other, 0, 0), + if (isSameTemplate(getMoreSpecializedTemplate(BestTemplate, Challenger, Loc, + TPOC_Other, 0), Challenger)) { Best = I; BestTemplate = Challenger; @@ -5749,7 +5773,7 @@ UnresolvedSetIterator Sema::getMostSpecialized( = cast<FunctionDecl>(*I)->getPrimaryTemplate(); if (I != Best && !isSameTemplate(getMoreSpecializedTemplate(BestTemplate, Challenger, - Loc, TPOC_Other, 0, 0), + Loc, TPOC_Other, 0), BestTemplate)) { Ambiguous = true; break; diff --git a/clang/test/CXX/drs/dr24xx.cpp b/clang/test/CXX/drs/dr24xx.cpp index ae8dda3351f48e..4534ed26e56d07 100644 --- a/clang/test/CXX/drs/dr24xx.cpp +++ b/clang/test/CXX/drs/dr24xx.cpp @@ -68,3 +68,64 @@ template<A> struct X {}; X<1> x; #endif } + +namespace dr2445 { // dr2445: 19 +#if __cplusplus >= 202002L + template <typename> constexpr bool F = false; + template <typename T> struct A { }; + + template <typename T, typename U> + bool operator==(T, A<U *>); + + template <typename T, typename U> + bool operator!=(A<T>, U) { + static_assert(F<T>, "Isn't this less specialized?"); + return false; + } + + bool f(A<int> ax, A<int *> ay) { return ay != ax; } + + template<class T> concept AlwaysTrue=true; + template <class T> struct B { + template <AlwaysTrue U> + bool operator==(const B<U>&)const; + }; + + + template <typename U> + bool operator==(const B<int>&,const B<U>&) { + static_assert(F<int>, "Isn't this less specialized?"); + return false; + } + + bool g(B<int> bx, B<int *> by) { return bx == by; } + + struct C{ + template<AlwaysTrue T> + int operator+(T){return 0;} + template<class T> + void operator-(T){} + }; + template<class T> + void operator+(C&&,T){} + template<AlwaysTrue T> + int operator-(C&&,T){return 0;} + + void t(int* iptr){ + int x1 = C{} + iptr; + int x2 = C{} - iptr; + } + + struct D{ + template<AlwaysTrue T> + int operator+(T) volatile {return 1;} + }; + + template<class T> + void operator+(volatile D&,T) {} + + int foo(volatile D& d){ + return d + 1; + } +#endif +} diff --git a/clang/test/SemaCXX/overload-template.cpp b/clang/test/SemaCXX/overload-template.cpp index 0a23788ef3da6a..0fe13c479cce22 100644 --- a/clang/test/SemaCXX/overload-template.cpp +++ b/clang/test/SemaCXX/overload-template.cpp @@ -1,4 +1,5 @@ // RUN: %clang_cc1 -fsyntax-only -verify %s +// RUN: %clang_cc1 -std=c++23 -verify -fsyntax-only %s enum copy_traits { movable = 1 }; @@ -33,3 +34,27 @@ void ReproducesBugSimply() { InsertRow(3, B{}); // expected-error {{no matching function for call to 'InsertRow'}} } +#if __cplusplus >= 202302L +namespace overloadCheck{ + template<typename T> + concept AlwaysTrue = true; + + struct S { + int f(AlwaysTrue auto) { return 1; } + void f(this S&&, auto) {} + + void g(auto) {} + int g(this S&&,AlwaysTrue auto) {return 1;} + + int h(AlwaysTrue auto) { return 1; } //expected-note {{previous definition is here}} + int h(this S&&,AlwaysTrue auto) { // expected-error {{class member cannot be redeclared}} + return 1; + } + }; + + int main() { + int x = S{}.f(0); + int y = S{}.g(0); + } +} +#endif diff --git a/clang/test/SemaCXX/overloaded-operator.cpp b/clang/test/SemaCXX/overloaded-operator.cpp index 887848c29b83c5..49311625d7ab2d 100644 --- a/clang/test/SemaCXX/overloaded-operator.cpp +++ b/clang/test/SemaCXX/overloaded-operator.cpp @@ -645,3 +645,40 @@ class b { } + +#if __cplusplus >= 202002L +namespace nw{ + template<class T> + concept AlwaysTrue=true; + + struct S{ + template<class T> + void operator+(const T&)const{} + + template<AlwaysTrue T> + int operator-(const T&)const{return 0;} + + template<AlwaysTrue T> + int operator*(const T&)const{ // expected-note {{candidate function}} + return 0; + } + }; + + template<AlwaysTrue T> + int operator+(const S&, const T&){return 0;} + + template<class T> + void operator-(const S&, const T&){} + + template<AlwaysTrue T> + int operator*(const S&, const T&){ // expected-note {{candidate function}} + return 0; + } + + void foo(){ + int a = S{} + 1; + int b = S{} - 1; + int c = S{} * 1; // expected-error {{use of overloaded operator '*' is ambiguous (with operand types 'S' and 'int')}} + } +} +#endif diff --git a/clang/www/cxx_dr_status.html b/clang/www/cxx_dr_status.html index 0b810b50c529c2..774c71bc1cb6b7 100755 --- a/clang/www/cxx_dr_status.html +++ b/clang/www/cxx_dr_status.html @@ -14478,7 +14478,7 @@ <h2 id="cxxdr">C++ defect report implementation status</h2> <td><a href="https://cplusplus.github.io/CWG/issues/2445.html">2445</a></td> <td>C++20</td> <td>Partial ordering with rewritten candidates</td> - <td class="unknown" align="center">Unknown</td> + <td class="unreleased" align="center">Clang 19</td> </tr> <tr id="2446"> <td><a href="https://cplusplus.github.io/CWG/issues/2446.html">2446</a></td> _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits