[clang] ae48d1c - [P0857R0 Part-B] Allows `require' clauses appearing in
Erich Keane via cfe-commits
cfe-commits at lists.llvm.org
Thu Oct 27 06:41:48 PDT 2022
Author: Liming Liu
Date: 2022-10-27T06:41:43-07:00
New Revision: ae48d1c76a6c14cdbf3a3e73f6c2c2befd0a81b0
URL: https://github.com/llvm/llvm-project/commit/ae48d1c76a6c14cdbf3a3e73f6c2c2befd0a81b0
DIFF: https://github.com/llvm/llvm-project/commit/ae48d1c76a6c14cdbf3a3e73f6c2c2befd0a81b0.diff
LOG: [P0857R0 Part-B] Allows `require' clauses appearing in
template-template parameters. Although it effects whether a template can be
used as an argument for another template, the constraint seems not to
be checked, nor other major implementations (GCC, MSVC, et al.) check it.
Additionally, Part-A of the document seems to have been implemented.
So mark P0857R0 as completed.
Differential Revision: https://reviews.llvm.org/D134128
Added:
Modified:
clang/docs/ReleaseNotes.rst
clang/include/clang/Sema/Sema.h
clang/lib/Parse/ParseTemplate.cpp
clang/lib/Sema/SemaConcept.cpp
clang/lib/Sema/SemaOverload.cpp
clang/lib/Sema/SemaTemplate.cpp
clang/lib/Sema/SemaTemplateInstantiate.cpp
clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
clang/test/CXX/temp/temp.arg/temp.arg.template/p3-2a.cpp
clang/test/SemaTemplate/concepts.cpp
clang/www/cxx_status.html
Removed:
################################################################################
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index ebed6186a1f02..4a2f1f62a6bed 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -556,9 +556,10 @@ C++20 Feature Support
- Implemented `P0634r3 <https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0634r3.html>`_,
which removes the requirement for the ``typename`` keyword in certain contexts.
- Implemented The Equality Operator You Are Looking For (`P2468 <http://wg21.link/p2468r2>`_).
-
- Implemented `P2113R0: Proposed resolution for 2019 comment CA 112 <https://wg21.link/P2113R0>`_
([temp.func.order]p6.2.1 is not implemented, matching GCC).
+- Implemented `P0857R0 <https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0857r0.html>`_,
+ which specifies constrained lambdas and constrained template *template-parameter*\s.
- Do not hide templated base members introduced via using-decl in derived class
(useful specially for constrained members). Fixes `GH50886 <https://github.com/llvm/llvm-project/issues/50886>`_.
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 9bf1ba7e8cc7d..b7a52aeea592c 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -7232,8 +7232,8 @@ class Sema final {
/// at least constrained than D2, and false otherwise.
///
/// \returns true if an error occurred, false otherwise.
- bool IsAtLeastAsConstrained(NamedDecl *D1, ArrayRef<const Expr *> AC1,
- NamedDecl *D2, ArrayRef<const Expr *> AC2,
+ bool IsAtLeastAsConstrained(NamedDecl *D1, MutableArrayRef<const Expr *> AC1,
+ NamedDecl *D2, MutableArrayRef<const Expr *> AC2,
bool &Result);
/// If D1 was not at least as constrained as D2, but would've been if a pair
@@ -9076,7 +9076,8 @@ class Sema final {
const TemplateArgumentList *Innermost = nullptr,
bool RelativeToPrimary = false,
const FunctionDecl *Pattern = nullptr,
- bool ForConstraintInstantiation = false);
+ bool ForConstraintInstantiation = false,
+ bool SkipForSpecialization = false);
/// A context in which code is being synthesized (where a source location
/// alone is not sufficient to identify the context). This covers template
@@ -9860,7 +9861,8 @@ class Sema final {
TemplateParameterList *
SubstTemplateParams(TemplateParameterList *Params, DeclContext *Owner,
- const MultiLevelTemplateArgumentList &TemplateArgs);
+ const MultiLevelTemplateArgumentList &TemplateArgs,
+ bool EvaluateConstraints = true);
bool
SubstTemplateArguments(ArrayRef<TemplateArgumentLoc> Args,
diff --git a/clang/lib/Parse/ParseTemplate.cpp b/clang/lib/Parse/ParseTemplate.cpp
index 138117e1a31ab..03f5d0b5a5f1c 100644
--- a/clang/lib/Parse/ParseTemplate.cpp
+++ b/clang/lib/Parse/ParseTemplate.cpp
@@ -874,27 +874,39 @@ NamedDecl *Parser::ParseTypeParameter(unsigned Depth, unsigned Position) {
/// template parameters.
///
/// type-parameter: [C++ temp.param]
-/// 'template' '<' template-parameter-list '>' type-parameter-key
-/// ...[opt] identifier[opt]
-/// 'template' '<' template-parameter-list '>' type-parameter-key
-/// identifier[opt] = id-expression
+/// template-head type-parameter-key ...[opt] identifier[opt]
+/// template-head type-parameter-key identifier[opt] = id-expression
/// type-parameter-key:
/// 'class'
/// 'typename' [C++1z]
-NamedDecl *
-Parser::ParseTemplateTemplateParameter(unsigned Depth, unsigned Position) {
+/// template-head: [C++2a]
+/// 'template' '<' template-parameter-list '>'
+/// requires-clause[opt]
+NamedDecl *Parser::ParseTemplateTemplateParameter(unsigned Depth,
+ unsigned Position) {
assert(Tok.is(tok::kw_template) && "Expected 'template' keyword");
// Handle the template <...> part.
SourceLocation TemplateLoc = ConsumeToken();
SmallVector<NamedDecl*,8> TemplateParams;
SourceLocation LAngleLoc, RAngleLoc;
+ ExprResult OptionalRequiresClauseConstraintER;
{
MultiParseScope TemplateParmScope(*this);
if (ParseTemplateParameters(TemplateParmScope, Depth + 1, TemplateParams,
LAngleLoc, RAngleLoc)) {
return nullptr;
}
+ if (TryConsumeToken(tok::kw_requires)) {
+ OptionalRequiresClauseConstraintER =
+ Actions.ActOnRequiresClause(ParseConstraintLogicalOrExpression(
+ /*IsTrailingRequiresClause=*/false));
+ if (!OptionalRequiresClauseConstraintER.isUsable()) {
+ SkipUntil(tok::comma, tok::greater, tok::greatergreater,
+ StopAtSemi | StopBeforeMatch);
+ return nullptr;
+ }
+ }
}
// Provide an ExtWarn if the C++1z feature of using 'typename' here is used.
@@ -956,11 +968,9 @@ Parser::ParseTemplateTemplateParameter(unsigned Depth, unsigned Position) {
if (TryConsumeToken(tok::ellipsis, EllipsisLoc))
DiagnoseMisplacedEllipsis(EllipsisLoc, NameLoc, AlreadyHasEllipsis, true);
- TemplateParameterList *ParamList =
- Actions.ActOnTemplateParameterList(Depth, SourceLocation(),
- TemplateLoc, LAngleLoc,
- TemplateParams,
- RAngleLoc, nullptr);
+ TemplateParameterList *ParamList = Actions.ActOnTemplateParameterList(
+ Depth, SourceLocation(), TemplateLoc, LAngleLoc, TemplateParams,
+ RAngleLoc, OptionalRequiresClauseConstraintER.get());
// Grab a default argument (if available).
// Per C++0x [basic.scope.pdecl]p9, we parse the default argument before
diff --git a/clang/lib/Sema/SemaConcept.cpp b/clang/lib/Sema/SemaConcept.cpp
index ff1f650a52084..9a919116a3c64 100644
--- a/clang/lib/Sema/SemaConcept.cpp
+++ b/clang/lib/Sema/SemaConcept.cpp
@@ -586,12 +586,13 @@ bool Sema::CheckFunctionConstraints(const FunctionDecl *FD,
// Figure out the to-translation-unit depth for this function declaration for
// the purpose of seeing if they
diff er by constraints. This isn't the same as
// getTemplateDepth, because it includes already instantiated parents.
-static unsigned CalculateTemplateDepthForConstraints(Sema &S,
- const NamedDecl *ND) {
+static unsigned
+CalculateTemplateDepthForConstraints(Sema &S, const NamedDecl *ND,
+ bool SkipForSpecialization = false) {
MultiLevelTemplateArgumentList MLTAL = S.getTemplateInstantiationArgs(
ND, /*Final=*/false, /*Innermost=*/nullptr, /*RelativeToPrimary=*/true,
/*Pattern=*/nullptr,
- /*ForConstraintInstantiation=*/true);
+ /*ForConstraintInstantiation=*/true, SkipForSpecialization);
return MLTAL.getNumSubstitutedLevels();
}
@@ -1278,8 +1279,10 @@ static bool subsumes(Sema &S, NamedDecl *DP, ArrayRef<const Expr *> P,
return false;
}
-bool Sema::IsAtLeastAsConstrained(NamedDecl *D1, ArrayRef<const Expr *> AC1,
- NamedDecl *D2, ArrayRef<const Expr *> AC2,
+bool Sema::IsAtLeastAsConstrained(NamedDecl *D1,
+ MutableArrayRef<const Expr *> AC1,
+ NamedDecl *D2,
+ MutableArrayRef<const Expr *> AC2,
bool &Result) {
if (AC1.empty()) {
Result = AC2.empty();
@@ -1298,6 +1301,21 @@ bool Sema::IsAtLeastAsConstrained(NamedDecl *D1, ArrayRef<const Expr *> AC1,
return false;
}
+ unsigned Depth1 = CalculateTemplateDepthForConstraints(*this, D1, true);
+ unsigned Depth2 = CalculateTemplateDepthForConstraints(*this, D2, true);
+
+ for (size_t I = 0; I != AC1.size() && I != AC2.size(); ++I) {
+ if (Depth2 > Depth1) {
+ AC1[I] = AdjustConstraintDepth(*this, Depth2 - Depth1)
+ .TransformExpr(const_cast<Expr *>(AC1[I]))
+ .get();
+ } else if (Depth1 > Depth2) {
+ AC2[I] = AdjustConstraintDepth(*this, Depth1 - Depth2)
+ .TransformExpr(const_cast<Expr *>(AC2[I]))
+ .get();
+ }
+ }
+
if (subsumes(*this, D1, AC1, D2, AC2, Result,
[this] (const AtomicConstraint &A, const AtomicConstraint &B) {
return A.subsumes(Context, B);
diff --git a/clang/lib/Sema/SemaOverload.cpp b/clang/lib/Sema/SemaOverload.cpp
index 8d044c32bb216..2190720bdc2de 100644
--- a/clang/lib/Sema/SemaOverload.cpp
+++ b/clang/lib/Sema/SemaOverload.cpp
@@ -10049,13 +10049,13 @@ bool clang::isBetterOverloadCandidate(
// parameter-type-lists, and F1 is more constrained than F2 [...],
if (!Cand1IsSpecialization && !Cand2IsSpecialization &&
sameFunctionParameterTypeLists(S, Cand1, Cand2)) {
- Expr *RC1 = Cand1.Function->getTrailingRequiresClause();
- Expr *RC2 = Cand2.Function->getTrailingRequiresClause();
+ const Expr *RC1 = Cand1.Function->getTrailingRequiresClause();
+ const Expr *RC2 = Cand2.Function->getTrailingRequiresClause();
if (RC1 && RC2) {
bool AtLeastAsConstrained1, AtLeastAsConstrained2;
- if (S.IsAtLeastAsConstrained(Cand1.Function, {RC1}, Cand2.Function, {RC2},
+ if (S.IsAtLeastAsConstrained(Cand1.Function, RC1, Cand2.Function, RC2,
AtLeastAsConstrained1) ||
- S.IsAtLeastAsConstrained(Cand2.Function, {RC2}, Cand1.Function, {RC1},
+ S.IsAtLeastAsConstrained(Cand2.Function, RC2, Cand1.Function, RC1,
AtLeastAsConstrained2))
return false;
if (AtLeastAsConstrained1 != AtLeastAsConstrained2)
diff --git a/clang/lib/Sema/SemaTemplate.cpp b/clang/lib/Sema/SemaTemplate.cpp
index 5cdc0bafbeec2..802c9574745f0 100644
--- a/clang/lib/Sema/SemaTemplate.cpp
+++ b/clang/lib/Sema/SemaTemplate.cpp
@@ -5717,7 +5717,8 @@ bool Sema::CheckTemplateArgument(
Params =
SubstTemplateParams(Params, CurContext,
MultiLevelTemplateArgumentList(
- Template, SugaredConverted, /*Final=*/true));
+ Template, SugaredConverted, /*Final=*/true),
+ /*EvaluateConstraints=*/false);
if (!Params)
return true;
}
diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp b/clang/lib/Sema/SemaTemplateInstantiate.cpp
index 6d89f5e968aff..9e41dfbfdbe95 100644
--- a/clang/lib/Sema/SemaTemplateInstantiate.cpp
+++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp
@@ -75,7 +75,8 @@ struct Response {
// Add template arguments from a variable template instantiation.
Response
HandleVarTemplateSpec(const VarTemplateSpecializationDecl *VarTemplSpec,
- MultiLevelTemplateArgumentList &Result) {
+ MultiLevelTemplateArgumentList &Result,
+ bool SkipForSpecialization) {
// For a class-scope explicit specialization, there are no template arguments
// at this level, but there may be enclosing template arguments.
if (VarTemplSpec->isClassScopeExplicitSpecialization())
@@ -93,16 +94,18 @@ HandleVarTemplateSpec(const VarTemplateSpecializationDecl *VarTemplSpec,
Specialized = VarTemplSpec->getSpecializedTemplateOrPartial();
if (VarTemplatePartialSpecializationDecl *Partial =
Specialized.dyn_cast<VarTemplatePartialSpecializationDecl *>()) {
- Result.addOuterTemplateArguments(
- Partial, VarTemplSpec->getTemplateInstantiationArgs().asArray(),
- /*Final=*/false);
+ if (!SkipForSpecialization)
+ Result.addOuterTemplateArguments(
+ Partial, VarTemplSpec->getTemplateInstantiationArgs().asArray(),
+ /*Final=*/false);
if (Partial->isMemberSpecialization())
return Response::Done();
} else {
VarTemplateDecl *Tmpl = Specialized.get<VarTemplateDecl *>();
- Result.addOuterTemplateArguments(
- Tmpl, VarTemplSpec->getTemplateInstantiationArgs().asArray(),
- /*Final=*/false);
+ if (!SkipForSpecialization)
+ Result.addOuterTemplateArguments(
+ Tmpl, VarTemplSpec->getTemplateInstantiationArgs().asArray(),
+ /*Final=*/false);
if (Tmpl->isMemberSpecialization())
return Response::Done();
}
@@ -126,17 +129,19 @@ HandleDefaultTempArgIntoTempTempParam(const TemplateTemplateParmDecl *TTP,
// Add template arguments from a class template instantiation.
Response
HandleClassTemplateSpec(const ClassTemplateSpecializationDecl *ClassTemplSpec,
- MultiLevelTemplateArgumentList &Result) {
+ MultiLevelTemplateArgumentList &Result,
+ bool SkipForSpecialization) {
if (!ClassTemplSpec->isClassScopeExplicitSpecialization()) {
// We're done when we hit an explicit specialization.
if (ClassTemplSpec->getSpecializationKind() == TSK_ExplicitSpecialization &&
!isa<ClassTemplatePartialSpecializationDecl>(ClassTemplSpec))
return Response::Done();
- Result.addOuterTemplateArguments(
- const_cast<ClassTemplateSpecializationDecl *>(ClassTemplSpec),
- ClassTemplSpec->getTemplateInstantiationArgs().asArray(),
- /*Final=*/false);
+ if (!SkipForSpecialization)
+ Result.addOuterTemplateArguments(
+ const_cast<ClassTemplateSpecializationDecl *>(ClassTemplSpec),
+ ClassTemplSpec->getTemplateInstantiationArgs().asArray(),
+ /*Final=*/false);
// If this class template specialization was instantiated from a
// specialized member that is a class template, we're done.
@@ -279,7 +284,7 @@ Response HandleGenericDeclContext(const Decl *CurDecl) {
MultiLevelTemplateArgumentList Sema::getTemplateInstantiationArgs(
const NamedDecl *ND, bool Final, const TemplateArgumentList *Innermost,
bool RelativeToPrimary, const FunctionDecl *Pattern,
- bool ForConstraintInstantiation) {
+ bool ForConstraintInstantiation, bool SkipForSpecialization) {
assert(ND && "Can't find arguments for a decl if one isn't provided");
// Accumulate the set of template argument lists in this structure.
MultiLevelTemplateArgumentList Result;
@@ -295,10 +300,11 @@ MultiLevelTemplateArgumentList Sema::getTemplateInstantiationArgs(
Response R;
if (const auto *VarTemplSpec =
dyn_cast<VarTemplateSpecializationDecl>(CurDecl)) {
- R = HandleVarTemplateSpec(VarTemplSpec, Result);
+ R = HandleVarTemplateSpec(VarTemplSpec, Result, SkipForSpecialization);
} else if (const auto *ClassTemplSpec =
dyn_cast<ClassTemplateSpecializationDecl>(CurDecl)) {
- R = HandleClassTemplateSpec(ClassTemplSpec, Result);
+ R = HandleClassTemplateSpec(ClassTemplSpec, Result,
+ SkipForSpecialization);
} else if (const auto *Function = dyn_cast<FunctionDecl>(CurDecl)) {
R = HandleFunction(Function, Result, Pattern, RelativeToPrimary,
ForConstraintInstantiation);
diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
index e471cb3de81c6..e034094431bb7 100644
--- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
+++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
@@ -4097,8 +4097,10 @@ TemplateDeclInstantiator::SubstTemplateParams(TemplateParameterList *L) {
TemplateParameterList *
Sema::SubstTemplateParams(TemplateParameterList *Params, DeclContext *Owner,
- const MultiLevelTemplateArgumentList &TemplateArgs) {
+ const MultiLevelTemplateArgumentList &TemplateArgs,
+ bool EvaluateConstraints) {
TemplateDeclInstantiator Instantiator(*this, Owner, TemplateArgs);
+ Instantiator.setEvaluateConstraints(EvaluateConstraints);
return Instantiator.SubstTemplateParams(Params);
}
diff --git a/clang/test/CXX/temp/temp.arg/temp.arg.template/p3-2a.cpp b/clang/test/CXX/temp/temp.arg/temp.arg.template/p3-2a.cpp
index 8e69f134a3d14..449b6232542e2 100644
--- a/clang/test/CXX/temp/temp.arg/temp.arg.template/p3-2a.cpp
+++ b/clang/test/CXX/temp/temp.arg/temp.arg.template/p3-2a.cpp
@@ -1,22 +1,27 @@
// RUN: %clang_cc1 -std=c++2a -frelaxed-template-template-args -verify %s
-template<typename T> concept C = T::f();
-// expected-note at -1{{similar constraint}}
+template<typename T> concept C = T::f(); // #C
template<typename T> concept D = C<T> && T::g();
-template<typename T> concept F = T::f();
-// expected-note at -1{{similar constraint expressions not considered equivalent}}
-template<template<C> class P> struct S1 { }; // expected-note 2{{'P' declared here}}
+template<typename T> concept F = T::f(); // #F
+template<template<C> class P> struct S1 { }; // #S1
template<C> struct X { };
-template<D> struct Y { }; // expected-note{{'Y' declared here}}
+template<D> struct Y { }; // #Y
template<typename T> struct Z { };
-template<F> struct W { }; // expected-note{{'W' declared here}}
-
+template<F> struct W { }; // #W
S1<X> s11;
-S1<Y> s12; // expected-error{{template template argument 'Y' is more constrained than template template parameter 'P'}}
+S1<Y> s12;
+// expected-error at -1 {{template template argument 'Y' is more constrained than template template parameter 'P'}}
+// expected-note@#S1 {{'P' declared here}}
+// expected-note@#Y {{'Y' declared here}}
S1<Z> s13;
-S1<W> s14; // expected-error{{template template argument 'W' is more constrained than template template parameter 'P'}}
+S1<W> s14;
+// expected-error at -1 {{template template argument 'W' is more constrained than template template parameter 'P'}}
+// expected-note@#S1 {{'P' declared here}}
+// expected-note@#W {{'W' declared here}}
+// expected-note@#F 1-2{{similar constraint expressions not considered equivalent}}
+// expected-note@#C 1-2{{similar constraint}}
template<template<typename> class P> struct S2 { };
@@ -32,3 +37,25 @@ using N = typename T::type;
using s31 = S3<N>;
using s32 = S3<Z>;
+
+template<template<typename T> requires C<T> class P> struct S4 { }; // #S4
+
+S4<X> s41;
+S4<Y> s42;
+// expected-error at -1 {{template template argument 'Y' is more constrained than template template parameter 'P'}}
+// expected-note@#S4 {{'P' declared here}}
+// expected-note@#Y {{'Y' declared here}}
+S4<Z> s43;
+S4<W> s44;
+// expected-error at -1 {{template template argument 'W' is more constrained than template template parameter 'P'}}
+// expected-note@#S4 {{'P' declared here}}
+// expected-note@#W {{'W' declared here}}
+
+template<template<typename T> requires C<T> typename U> struct S5 {
+ template<typename T> static U<T> V;
+};
+
+struct Nothing {};
+
+// FIXME: Wait the standard to clarify the intent.
+template<> template<> Z<Nothing> S5<Z>::V<Nothing>;
diff --git a/clang/test/SemaTemplate/concepts.cpp b/clang/test/SemaTemplate/concepts.cpp
index 362b429fe2758..64e21c4a49627 100644
--- a/clang/test/SemaTemplate/concepts.cpp
+++ b/clang/test/SemaTemplate/concepts.cpp
@@ -59,11 +59,10 @@ namespace P0857R0 {
x.operator()<false>(); // expected-error {{no matching member function}}
}
- // FIXME: This is valid under P0857R0.
template<typename T> concept C = true;
- template<template<typename T> requires C<T> typename U> struct X {}; // expected-error {{requires 'class'}} expected-error 0+{{}}
+ template<template<typename T> requires C<T> typename U> struct X {};
template<typename T> requires C<T> struct Y {};
- X<Y> xy; // expected-error {{no template named 'X'}}
+ X<Y> xy;
}
namespace PR50306 {
@@ -706,7 +705,7 @@ Container<4>::var_templ<int> inst;
Container<5>::var_templ<int> inst_fail;
// expected-error at -1{{constraints not satisfied for alias template 'var_templ'}}
// expected-note@#CMVT_REQ{{because 'sizeof(int) == arity' (4 == 5) evaluated to false}}
-} // namespace ConstrainedMemberVarTemplate
+} // namespace ConstrainedMemberVarTemplate
// These should not diagnose, where we were unintentionally doing so before by
// checking trailing requires clause twice, yet not having the ability to the
@@ -764,4 +763,4 @@ struct __iterator_traits_member_pointer_or_arrow_or_void<_Ip> {
void use2() {
__iterator_traits_member_pointer_or_arrow_or_void<counted_iterator<int>> f;
}
-}// namespace InheritedFromPartialSpec
+}// namespace InheritedFromPartialSpec
diff --git a/clang/www/cxx_status.html b/clang/www/cxx_status.html
index d46e7bb0b46a4..b00b3b903e053 100755
--- a/clang/www/cxx_status.html
+++ b/clang/www/cxx_status.html
@@ -912,11 +912,7 @@ <h2 id="cxx20">C++20 implementation status</h2>
</tr>
<tr> <!-- from Albuquerque -->
<td><a href="https://wg21.link/p0857r0">P0857R0</a></td>
- <td class="partial" align="center">
- <details><summary>Partial</summary>
- Constraining template template parameters is not yet supported.
- </details>
- </td>
+ <td rowspan="1" class="unreleased" align="center">Clang 16</td>
</tr>
<tr> <!-- from San Diego -->
<td><a href="https://wg21.link/p1084r2">P1084R2</a></td>
More information about the cfe-commits
mailing list