Improve template instantiation for member access expressions that

involve qualified names, e.g., x->Base::f. We now maintain enough
information in the AST to compare the results of the name lookup of
"Base" in the scope of the postfix-expression (determined at template
definition time) and in the type of the object expression.

llvm-svn: 80953
This commit is contained in:
Douglas Gregor 2009-09-03 21:38:09 +00:00
parent 59a1cd4a06
commit 2b6ca46c6b
10 changed files with 236 additions and 85 deletions

View File

@ -1306,6 +1306,15 @@ class CXXUnresolvedMemberExpr : public Expr {
/// \brief The source range covering the nested name specifier.
SourceRange QualifierRange;
/// \brief In a qualified member access expression such as t->Base::f, this
/// member stores the resolves of name lookup in the context of the member
/// access expression, to be used at instantiation time.
///
/// FIXME: This member, along with the Qualifier and QualifierRange, could
/// be stuck into a structure that is optionally allocated at the end of
/// the CXXUnresolvedMemberExpr, to save space in the common case.
NamedDecl *FirstQualifierFoundInScope;
/// \brief The member to which this member expression refers, which
/// can be name, overloaded operator, or destructor.
/// FIXME: could also be a template-id
@ -1320,11 +1329,13 @@ public:
SourceLocation OperatorLoc,
NestedNameSpecifier *Qualifier,
SourceRange QualifierRange,
NamedDecl *FirstQualifierFoundInScope,
DeclarationName Member,
SourceLocation MemberLoc)
: Expr(CXXUnresolvedMemberExprClass, C.DependentTy, true, true),
Base(Base), IsArrow(IsArrow), OperatorLoc(OperatorLoc),
Qualifier(Qualifier), QualifierRange(QualifierRange),
FirstQualifierFoundInScope(FirstQualifierFoundInScope),
Member(Member), MemberLoc(MemberLoc) { }
/// \brief Retrieve the base object of this member expressions,
@ -1349,6 +1360,21 @@ public:
/// that qualifies the member name.
SourceRange getQualifierRange() const { return QualifierRange; }
/// \brief Retrieve the first part of the nested-name-specifier that was
/// found in the scope of the member access expression when the member access
/// was initially parsed.
///
/// This function only returns a useful result when member access expression
/// uses a qualified member name, e.g., "x.Base::f". Here, the declaration
/// returned by this function describes what was found by unqualified name
/// lookup for the identifier "Base" within the scope of the member access
/// expression itself. At template instantiation time, this information is
/// combined with the results of name lookup into the type of the object
/// expression itself (the class type of x).
NamedDecl *getFirstQualifierFoundInScope() const {
return FirstQualifierFoundInScope;
}
/// \brief Retrieve the name of the member that this expression
/// refers to.
DeclarationName getMember() const { return Member; }

View File

@ -1610,13 +1610,15 @@ public:
SourceLocation MemberLoc,
DeclarationName MemberName,
DeclPtrTy ImplDecl,
const CXXScopeSpec *SS = 0) {
const CXXScopeSpec *SS = 0,
NamedDecl *FirstQualifierInScope = 0) {
// FIXME: Temporary helper while we migrate existing calls to
// BuildMemberReferenceExpr to support explicitly-specified template
// arguments.
return BuildMemberReferenceExpr(S, move(Base), OpLoc, OpKind, MemberLoc,
MemberName, false, SourceLocation(), 0, 0,
SourceLocation(), ImplDecl, SS);
SourceLocation(), ImplDecl, SS,
FirstQualifierInScope);
}
OwningExprResult BuildMemberReferenceExpr(Scope *S, ExprArg Base,
@ -1630,7 +1632,8 @@ public:
unsigned NumExplicitTemplateArgs,
SourceLocation RAngleLoc,
DeclPtrTy ImplDecl,
const CXXScopeSpec *SS = 0);
const CXXScopeSpec *SS,
NamedDecl *FirstQualifierInScope = 0);
virtual OwningExprResult ActOnMemberReferenceExpr(Scope *S, ExprArg Base,
SourceLocation OpLoc,
@ -2047,12 +2050,18 @@ public:
virtual CXXScopeTy *ActOnCXXGlobalScopeSpecifier(Scope *S,
SourceLocation CCLoc);
/// ActOnCXXNestedNameSpecifier - Called during parsing of a
/// nested-name-specifier. e.g. for "foo::bar::" we parsed "foo::" and now
/// we want to resolve "bar::". 'SS' is empty or the previously parsed
/// nested-name part ("foo::"), 'IdLoc' is the source location of 'bar',
/// 'CCLoc' is the location of '::' and 'II' is the identifier for 'bar'.
/// Returns a CXXScopeTy* object representing the C++ scope.
NamedDecl *FindFirstQualifierInScope(Scope *S, NestedNameSpecifier *NNS);
CXXScopeTy *BuildCXXNestedNameSpecifier(Scope *S,
const CXXScopeSpec &SS,
SourceLocation IdLoc,
SourceLocation CCLoc,
IdentifierInfo &II,
QualType ObjectType,
NamedDecl *ScopeLookupResult,
bool EnteringContext);
virtual CXXScopeTy *ActOnCXXNestedNameSpecifier(Scope *S,
const CXXScopeSpec &SS,
SourceLocation IdLoc,

View File

@ -288,18 +288,45 @@ bool isAcceptableNestedNameSpecifier(ASTContext &Context, NamedDecl *SD) {
return false;
}
/// ActOnCXXNestedNameSpecifier - Called during parsing of a
/// nested-name-specifier. e.g. for "foo::bar::" we parsed "foo::" and now
/// we want to resolve "bar::". 'SS' is empty or the previously parsed
/// nested-name part ("foo::"), 'IdLoc' is the source location of 'bar',
/// 'CCLoc' is the location of '::' and 'II' is the identifier for 'bar'.
/// Returns a CXXScopeTy* object representing the C++ scope.
Sema::CXXScopeTy *Sema::ActOnCXXNestedNameSpecifier(Scope *S,
/// \brief If the given nested-name-specifier begins with a bare identifier
/// (e.g., Base::), perform name lookup for that identifier as a
/// nested-name-specifier within the given scope, and return the result of that
/// name lookup.
NamedDecl *Sema::FindFirstQualifierInScope(Scope *S, NestedNameSpecifier *NNS) {
if (!S || !NNS)
return 0;
while (NNS->getPrefix())
NNS = NNS->getPrefix();
if (NNS->getKind() != NestedNameSpecifier::Identifier)
return 0;
LookupResult Found
= LookupName(S, NNS->getAsIdentifier(), LookupNestedNameSpecifierName);
assert(!Found.isAmbiguous() && "Cannot handle ambiguities here yet");
NamedDecl *Result = Found;
if (isAcceptableNestedNameSpecifier(Context, Result))
return Result;
return 0;
}
/// \brief Build a new nested-name-specifier for "identifier::", as described
/// by ActOnCXXNestedNameSpecifier.
///
/// This routine differs only slightly from ActOnCXXNestedNameSpecifier, in
/// that it contains an extra parameter \p ScopeLookupResult, which provides
/// the result of name lookup within the scope of the nested-name-specifier
/// that was computed at template definitino time.
Sema::CXXScopeTy *Sema::BuildCXXNestedNameSpecifier(Scope *S,
const CXXScopeSpec &SS,
SourceLocation IdLoc,
SourceLocation CCLoc,
IdentifierInfo &II,
TypeTy *ObjectTypePtr,
QualType ObjectType,
NamedDecl *ScopeLookupResult,
bool EnteringContext) {
NestedNameSpecifier *Prefix
= static_cast<NestedNameSpecifier *>(SS.getScopeRep());
@ -307,11 +334,10 @@ Sema::CXXScopeTy *Sema::ActOnCXXNestedNameSpecifier(Scope *S,
// Determine where to perform name lookup
DeclContext *LookupCtx = 0;
bool isDependent = false;
if (ObjectTypePtr) {
if (!ObjectType.isNull()) {
// This nested-name-specifier occurs in a member access expression, e.g.,
// x->B::f, and we are looking into the type of the object.
assert(!SS.isSet() && "ObjectType and scope specifier cannot coexist");
QualType ObjectType = QualType::getFromOpaquePtr(ObjectTypePtr);
LookupCtx = computeDeclContext(ObjectType);
isDependent = ObjectType->isDependentType();
} else if (SS.isSet()) {
@ -336,7 +362,7 @@ Sema::CXXScopeTy *Sema::ActOnCXXNestedNameSpecifier(Scope *S,
Found = LookupQualifiedName(LookupCtx, &II, LookupNestedNameSpecifierName,
false);
if (ObjectTypePtr && Found.getKind() == LookupResult::NotFound && S) {
if (!ObjectType.isNull() && Found.getKind() == LookupResult::NotFound) {
// C++ [basic.lookup.classref]p4:
// If the id-expression in a class member access is a qualified-id of
// the form
@ -354,12 +380,14 @@ Sema::CXXScopeTy *Sema::ActOnCXXNestedNameSpecifier(Scope *S,
// Qualified name lookup into a class will not find a namespace-name,
// so we do not need to diagnoste that case specifically. However,
// this qualified name lookup may find nothing. In that case, perform
// unqualified name lookup in the given scope.
// FIXME: When we're instantiating a template, do we actually have to
// look in the scope of the template? Both EDG and GCC do it; GCC
// requires the lookup to be successful, EDG doesn't.
// unqualified name lookup in the given scope (if available) or
// reconstruct the result from when name lookup was performed at template
// definition time.
if (S)
Found = LookupName(S, &II, LookupNestedNameSpecifierName);
else
Found = LookupResult::CreateLookupResult(Context, ScopeLookupResult);
ObjectTypeSearchedInScope = true;
}
} else if (isDependent) {
@ -379,16 +407,21 @@ Sema::CXXScopeTy *Sema::ActOnCXXNestedNameSpecifier(Scope *S,
// FIXME: Deal with ambiguities cleanly.
NamedDecl *SD = Found;
if (isAcceptableNestedNameSpecifier(Context, SD)) {
if (ObjectTypePtr && !ObjectTypeSearchedInScope && S) {
if (!ObjectType.isNull() && !ObjectTypeSearchedInScope) {
// C++ [basic.lookup.classref]p4:
// [...] If the name is found in both contexts, the
// class-name-or-namespace-name shall refer to the same entity.
//
// We already found the name in the scope of the object. Now, look
// into the current scope (the scope of the postfix-expression) to
// see if we can find the same name there.
LookupResult FoundOuter
= LookupName(S, &II, LookupNestedNameSpecifierName);
// see if we can find the same name there. As above, if there is no
// scope, reconstruct the result from the template instantiation itself.
LookupResult FoundOuter;
if (S)
FoundOuter = LookupName(S, &II, LookupNestedNameSpecifierName);
else
FoundOuter = LookupResult::CreateLookupResult(Context,
ScopeLookupResult);
// FIXME: Handle ambiguities in FoundOuter!
NamedDecl *OuterDecl = FoundOuter;
@ -401,7 +434,7 @@ Sema::CXXScopeTy *Sema::ActOnCXXNestedNameSpecifier(Scope *S,
Diag(IdLoc, diag::err_nested_name_member_ref_lookup_ambiguous)
<< &II;
Diag(SD->getLocation(), diag::note_ambig_member_ref_object_type)
<< QualType::getFromOpaquePtr(ObjectTypePtr);
<< ObjectType;
Diag(OuterDecl->getLocation(), diag::note_ambig_member_ref_scope);
// Fall through so that we'll pick the name we found in the object type,
@ -450,6 +483,24 @@ Sema::CXXScopeTy *Sema::ActOnCXXNestedNameSpecifier(Scope *S,
return 0;
}
/// ActOnCXXNestedNameSpecifier - Called during parsing of a
/// nested-name-specifier. e.g. for "foo::bar::" we parsed "foo::" and now
/// we want to resolve "bar::". 'SS' is empty or the previously parsed
/// nested-name part ("foo::"), 'IdLoc' is the source location of 'bar',
/// 'CCLoc' is the location of '::' and 'II' is the identifier for 'bar'.
/// Returns a CXXScopeTy* object representing the C++ scope.
Sema::CXXScopeTy *Sema::ActOnCXXNestedNameSpecifier(Scope *S,
const CXXScopeSpec &SS,
SourceLocation IdLoc,
SourceLocation CCLoc,
IdentifierInfo &II,
TypeTy *ObjectTypePtr,
bool EnteringContext) {
return BuildCXXNestedNameSpecifier(S, SS, IdLoc, CCLoc, II,
QualType::getFromOpaquePtr(ObjectTypePtr),
/*ScopeLookupResult=*/0, EnteringContext);
}
Sema::CXXScopeTy *Sema::ActOnCXXNestedNameSpecifier(Scope *S,
const CXXScopeSpec &SS,
TypeTy *Ty,

View File

@ -1989,7 +1989,8 @@ Sema::BuildMemberReferenceExpr(Scope *S, ExprArg Base, SourceLocation OpLoc,
const TemplateArgument *ExplicitTemplateArgs,
unsigned NumExplicitTemplateArgs,
SourceLocation RAngleLoc,
DeclPtrTy ObjCImpDecl, const CXXScopeSpec *SS) {
DeclPtrTy ObjCImpDecl, const CXXScopeSpec *SS,
NamedDecl *FirstQualifierInScope) {
if (SS && SS->isInvalid())
return ExprError();
@ -2022,14 +2023,23 @@ Sema::BuildMemberReferenceExpr(Scope *S, ExprArg Base, SourceLocation OpLoc,
// Get the type being accessed in BaseType. If this is an arrow, the BaseExpr
// must have pointer type, and the accessed type is the pointee.
if (OpKind == tok::arrow) {
if (BaseType->isDependentType())
if (BaseType->isDependentType()) {
NestedNameSpecifier *Qualifier = 0;
if (SS) {
Qualifier = static_cast<NestedNameSpecifier *>(SS->getScopeRep());
if (!FirstQualifierInScope)
FirstQualifierInScope = FindFirstQualifierInScope(S, Qualifier);
}
return Owned(new (Context) CXXUnresolvedMemberExpr(Context,
BaseExpr, true,
OpLoc,
(NestedNameSpecifier *)(SS? SS->getScopeRep() : 0),
Qualifier,
SS? SS->getRange() : SourceRange(),
FirstQualifierInScope,
MemberName,
MemberLoc));
}
else if (const PointerType *PT = BaseType->getAs<PointerType>())
BaseType = PT->getPointeeType();
else if (BaseType->isObjCObjectPointerType())
@ -2051,16 +2061,25 @@ Sema::BuildMemberReferenceExpr(Scope *S, ExprArg Base, SourceLocation OpLoc,
const PointerType *PT = BaseType->getAs<PointerType>();
if (!PT || (getLangOptions().ObjC1 &&
!PT->getPointeeType()->isRecordType()))
!PT->getPointeeType()->isRecordType())) {
NestedNameSpecifier *Qualifier = 0;
if (SS) {
Qualifier = static_cast<NestedNameSpecifier *>(SS->getScopeRep());
if (!FirstQualifierInScope)
FirstQualifierInScope = FindFirstQualifierInScope(S, Qualifier);
}
return Owned(new (Context) CXXUnresolvedMemberExpr(Context,
BaseExpr, false,
OpLoc,
(NestedNameSpecifier *)(SS? SS->getScopeRep() : 0),
Qualifier,
SS? SS->getRange() : SourceRange(),
FirstQualifierInScope,
MemberName,
MemberLoc));
}
}
}
// Handle field access to simple records. This also handles access to fields
// of the ObjC 'id' struct.

View File

@ -1789,9 +1789,20 @@ Sema::ActOnStartCXXMemberReference(Scope *S, ExprArg Base, SourceLocation OpLoc,
// We could end up with various non-record types here, such as extended
// vector types or Objective-C interfaces. Just return early and let
// ActOnMemberReferenceExpr do the work.
if (!BaseType->isRecordType())
if (!BaseType->isRecordType()) {
// C++ [basic.lookup.classref]p2:
// [...] If the type of the object expression is of pointer to scalar
// type, the unqualified-id is looked up in the context of the complete
// postfix-expression.
ObjectType = 0;
return move(Base);
}
// C++ [basic.lookup.classref]p2:
// If the id-expression in a class member access (5.2.5) is an
// unqualified-id, and the type of the object expres- sion is of a class
// type C (or of pointer to a class type C), the unqualified-id is looked
// up in the scope of class C. [...]
ObjectType = BaseType.getAsOpaquePtr();
return move(Base);
}

View File

@ -359,8 +359,10 @@ namespace {
}
Decl *TemplateInstantiator::TransformDecl(Decl *D) {
if (TemplateTemplateParmDecl *TTP
= dyn_cast_or_null<TemplateTemplateParmDecl>(D)) {
if (!D)
return 0;
if (TemplateTemplateParmDecl *TTP = dyn_cast<TemplateTemplateParmDecl>(D)) {
if (TTP->getDepth() < TemplateArgs.getNumLevels()) {
assert(TemplateArgs(TTP->getDepth(), TTP->getPosition()).getAsDecl() &&
"Wrong kind of template template argument");
@ -381,7 +383,7 @@ Decl *TemplateInstantiator::TransformDecl(Decl *D) {
"Reducing depth of template template parameters is not yet implemented");
}
return SemaRef.FindInstantiatedDecl(cast_or_null<NamedDecl>(D));
return SemaRef.FindInstantiatedDecl(cast<NamedDecl>(D));
}
Decl *TemplateInstantiator::TransformDefinition(Decl *D) {

View File

@ -15,6 +15,7 @@
#include "Sema.h"
#include "clang/Sema/SemaDiagnostic.h"
#include "clang/AST/Decl.h"
#include "clang/AST/Expr.h"
#include "clang/AST/ExprCXX.h"
#include "clang/AST/ExprObjC.h"
@ -243,7 +244,8 @@ public:
/// alternate behavior.
NestedNameSpecifier *TransformNestedNameSpecifier(NestedNameSpecifier *NNS,
SourceRange Range,
QualType ObjectType = QualType());
QualType ObjectType = QualType(),
NamedDecl *FirstQualifierInScope = 0);
/// \brief Transform the given template name.
///
@ -499,7 +501,8 @@ public:
NestedNameSpecifier *RebuildNestedNameSpecifier(NestedNameSpecifier *Prefix,
SourceRange Range,
IdentifierInfo &II,
QualType ObjectType);
QualType ObjectType,
NamedDecl *FirstQualifierInScope);
/// \brief Build a new nested-name-specifier given the prefix and the
/// namespace named in the next step in the nested-name-specifier.
@ -1454,7 +1457,8 @@ public:
NestedNameSpecifier *Qualifier,
SourceRange QualifierRange,
DeclarationName Name,
SourceLocation MemberLoc) {
SourceLocation MemberLoc,
NamedDecl *FirstQualifierInScope) {
OwningExprResult Base = move(BaseE);
tok::TokenKind OpKind = IsArrow? tok::arrow : tok::period;
@ -1467,7 +1471,8 @@ public:
MemberLoc,
Name,
/*FIXME?*/Sema::DeclPtrTy::make((Decl*)0),
&SS);
&SS,
FirstQualifierInScope);
return move(Base);
}
@ -1591,7 +1596,8 @@ template<typename Derived>
NestedNameSpecifier *
TreeTransform<Derived>::TransformNestedNameSpecifier(NestedNameSpecifier *NNS,
SourceRange Range,
QualType ObjectType) {
QualType ObjectType,
NamedDecl *FirstQualifierInScope) {
if (!NNS)
return 0;
@ -1599,13 +1605,15 @@ TreeTransform<Derived>::TransformNestedNameSpecifier(NestedNameSpecifier *NNS,
NestedNameSpecifier *Prefix = NNS->getPrefix();
if (Prefix) {
Prefix = getDerived().TransformNestedNameSpecifier(Prefix, Range,
ObjectType);
ObjectType,
FirstQualifierInScope);
if (!Prefix)
return 0;
// Clear out the object type; it only applies to the first element in
// the nested-name-specifier.
// Clear out the object type and the first qualifier in scope; they only
// apply to the first element in the nested-name-specifier.
ObjectType = QualType();
FirstQualifierInScope = 0;
}
switch (NNS->getKind()) {
@ -1618,7 +1626,8 @@ TreeTransform<Derived>::TransformNestedNameSpecifier(NestedNameSpecifier *NNS,
return getDerived().RebuildNestedNameSpecifier(Prefix, Range,
*NNS->getAsIdentifier(),
ObjectType);
ObjectType,
FirstQualifierInScope);
case NestedNameSpecifier::Namespace: {
NamespaceDecl *NS
@ -4055,11 +4064,16 @@ TreeTransform<Derived>::TransformCXXUnresolvedMemberExpr(
if (Base.isInvalid())
return SemaRef.ExprError();
NamedDecl *FirstQualifierInScope
= cast_or_null<NamedDecl>(
getDerived().TransformDecl(E->getFirstQualifierFoundInScope()));
NestedNameSpecifier *Qualifier = 0;
if (E->getQualifier()) {
Qualifier = getDerived().TransformNestedNameSpecifier(E->getQualifier(),
E->getQualifierRange(),
QualType::getFromOpaquePtr(ObjectType));
QualType::getFromOpaquePtr(ObjectType),
FirstQualifierInScope);
if (!Qualifier)
return SemaRef.ExprError();
}
@ -4070,7 +4084,8 @@ TreeTransform<Derived>::TransformCXXUnresolvedMemberExpr(
if (!getDerived().AlwaysRebuild() &&
Base.get() == E->getBase() &&
Qualifier == E->getQualifier() &&
Name == E->getMember())
Name == E->getMember() &&
FirstQualifierInScope == E->getFirstQualifierFoundInScope())
return SemaRef.Owned(E->Retain());
return getDerived().RebuildCXXUnresolvedMemberExpr(move(Base),
@ -4079,7 +4094,8 @@ TreeTransform<Derived>::TransformCXXUnresolvedMemberExpr(
Qualifier,
E->getQualifierRange(),
Name,
E->getMemberLoc());
E->getMemberLoc(),
FirstQualifierInScope);
}
template<typename Derived>
@ -4435,15 +4451,17 @@ NestedNameSpecifier *
TreeTransform<Derived>::RebuildNestedNameSpecifier(NestedNameSpecifier *Prefix,
SourceRange Range,
IdentifierInfo &II,
QualType ObjectType) {
QualType ObjectType,
NamedDecl *FirstQualifierInScope) {
CXXScopeSpec SS;
// FIXME: The source location information is all wrong.
SS.setRange(Range);
SS.setScopeRep(Prefix);
return static_cast<NestedNameSpecifier *>(
SemaRef.ActOnCXXNestedNameSpecifier(0, SS, Range.getEnd(),
SemaRef.BuildCXXNestedNameSpecifier(0, SS, Range.getEnd(),
Range.getEnd(), II,
ObjectType.getAsOpaquePtr(),
ObjectType,
FirstQualifierInScope,
false));
}

View File

@ -107,18 +107,26 @@ namespace C
a.A::B::base::x();
a->A::member::foo();
a.bad::x(); // xpected-error{{direct or virtual}}
a.sub::x();
a.base::x();
a.B::base::x(); // xpected-error{{use of undeclared identifier 'B'}}
a->member::foo();
a.bad::x(); // expected-error{{direct or virtual}}
}
void test_fun5() {
// FIXME: Enable the following once we get the nested-name-specifier lookup
// right during template instantiation.
// fun5<A::sub>(); // xpected-note 2{{instantiation}}
fun5<A::sub>(); // expected-note{{instantiation}}
}
template<typename T>
void fun6() {
T a;
a.sub::x();
a.base::x();
a->member::foo();
a.B::base::x(); // expected-error{{use of undeclared identifier 'B'}}
}
void test_fun6() {
fun6<A::sub>(); // expected-note{{instantiation}}
}
}
// PR4703

View File

@ -1,5 +1,4 @@
// RUN: clang-cc -fsyntax-only -verify %s
// XFAIL
template<typename T>
void call_f0(T x) {
x.Base::f0();
@ -30,7 +29,8 @@ void test_f0_through_typedef(X0 x0) {
template<typename TheBase, typename T>
void call_f0_through_typedef2(T x) {
typedef TheBase CrazyBase; // expected-note{{current scope}}
x.CrazyBase::f0(); // expected-error{{ambiguous}}
x.CrazyBase::f0(); // expected-error{{ambiguous}} \
// expected-error 2{{no member named}}
}
struct OtherBase { };
@ -41,8 +41,8 @@ struct X1 : Base, OtherBase {
void test_f0_through_typedef2(X0 x0, X1 x1) {
call_f0_through_typedef2<Base>(x0);
call_f0_through_typedef2<OtherBase>(x1);
call_f0_through_typedef2<Base>(x1); // expected-note{{here}}
call_f0_through_typedef2<OtherBase>(x1); // expected-note{{instantiation}}
call_f0_through_typedef2<Base>(x1); // expected-note{{instantiation}}
}

View File

@ -380,7 +380,14 @@ welcome!</p>
<td></td>
</tr>
<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;3.4.4 [basic.lookup.elab]</td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;3.4.5 [basic.lookup.classref]</td><td></td><td></td><td></td><td></td><td></td></tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;&nbsp;3.4.5 [basic.lookup.classref]</td>
<td class="na">N/A</td>
<td class="na">N/A</td>
<td class="advanced"></td>
<td class="na">N/A</td>
<td>Missing ambiguity/consistency checks for paragraphs 3 (~type-name) and 7 (conversion-type-id)</td>
</tr>
<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;3.4.6 [basic.lookup.udir]</td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td>&nbsp;&nbsp;3.5 [basic.link]</td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td>&nbsp;&nbsp;3.6 [basic.start]</td><td></td><td></td><td></td><td></td><td></td></tr>