Document number: |
00-0006/N1229 |
Date: |
March 2, 2000 |
Author: |
John Spicer, Edison Design Group |
|
jhs@edg.com |
Friend Declaration Issues
Introduction
This paper is primarily related to issue #138 -- whether using-directives
are considered when looking for a previous declaration of a name
declared in a friend declaration. The key issue here is how
a possible previous declaration of a class or function is found.
The wording in the standard regarding the handling of names declared
in unqualified friend declarations is unclear and, in certain cases,
clearly incorrect. At the Kona meeting (10/99) the core working group
agreed that, despite assertions to the contrary, the mechanism used to
match a friend function declaration with a possible prior declaration of the
class or function is not the normal lookup mechanism. This paper
explores the issues involved in this process and proposes changes to
the standard to clarify the issue.
This paper makes some assertions about "the way things are". For example,
the section below asserts that friend function declarations do not consider
declarations from base classes. These assertions are based on both
my understanding of how the language has worked over the years and
are backed up by verification of the code using several different
compilers (EDG, Sun 5.0, g++ 2.95.2, Microsoft 6.0 and Borland 5.4).
Issues
Declaration matching is not normal lookup
The key concept affecting issue #138 is the question of how a possible
previous declaration of a class or function is found. It has been
argued that the mechanism used is normal lookup, and that a uniform
set of rules should apply to both classes and functions.
In researching this issue I've determined that
-
the handling of classes and functions is vastly different
-
finding a prior declaration of a class is very close to normal lookup
-
finding a prior declaration of a function is very different from normal
lookup
This section will illustrate why normal lookup
produces undesirable results when used to find a prior declaration.
It will also show that, unfortunately, we can't apply the same
rules for both functions and classes. It would be great if we could
do so, but that would be a huge language change.
Normal lookup looks in base classes
When looking up a name in a class context, normal lookup looks in base
classes. When looking for a prior declaration of a friend function,
base classes are not inspected because the function declared is always
a namespace member:
// Is a friend function found in a base class?
struct C;
struct A {
int f(C);
};
struct C : public A {
friend int f(C);
};
C c;
int i = f(c); // only valid if the friend declares ::f(C)
Friend classes are handled differently. A friend class declaration
does look in base classes (i.e., friend class declarations are
consistent with normal lookup in this respect):
// Is a friend class found in a base class?
struct A { struct B {}; };
struct C : public A {
friend class B; // A::B, not ::B
};
Normal lookup looks in enclosing classes
When looking up a name in a class context, normal lookup looks in enclosing
classes. When looking for a prior declaration of a friend function,
enclosing classes are not inspected because the function declared is always
a namespace member:
// Is a friend function found in an enclosing class?
struct A {
struct B;
void f(B){}
struct B {
friend int f(B);
};
};
A::B b;
int i = f(b); // would fail if friend found B::f
Once again, friend classes are handled differently. A friend class declaration
does look in enclosing classes.
Normal lookup considers using-directives
When a name is looked up using the normal lookup rules, names made
visible by using-directives are considered. Using-directives are not
considered when looking for a possible prior declaration of a function,
however. In the following example, function i
is declared.
N::i
is not found (otherwise this code would be invalid).
namespace M {
namespace N {
int i;
int j;
}
using namespace N;
void i(){}
class X {
friend void j(){}
};
}
It seems clear, at least for the function case, that names made
visible from using-directives should not be considered. But what about
the class case? 7.3.1.2p3 says
When looking for a prior declaration of a class or function declared as a
friend, scopes outside the innermost enclosing namespace scope are not
considered.
This rule was added to make sure that the meaning of a friend declaration
could not be changed by adding a declaration to a namespace other than
the one that immediately contains the class.
In the following example, the EDG compiler says that the reference to
A
is ambiguous and that OA
refers to M::OA
.
The other compilers treat the friend declarations as references to
the names made visible by the using-directives. I consider this to
be wrong, but it could be argued that this is the correct behavior for
N::A
. It is clearly wrong for O::OA
though
because this is only visible if you look in scopes outside of the
innermost enclosing namespace (for name lookup purposes the members of
namespace O
are treated as members of the global namespace).
namespace O {
class OA {};
}
namespace M {
namespace N {
class A {};
}
using namespace N;
using namespace O;
struct B {
friend class A;
friend class OA;
};
A a; // N::A or ambiguous?
OA oa; // Always M::OA?
}
Declaration matching finds "invisible" declarations
There are certain consistency rules that must be applied to repeated
declarations of a function even if the previous declarations are not
actually visible. For example,
struct A {
friend void f(int) throw(int);
};
struct B {
friend void f(int) throw(char);
};
In this example an error must be issued on the second declaration of
f(int) because its throw specification is incompatible with the
previous declaration. The fact that the previous declaration can
be found to perform this error check indicates that the mechanism
used for declaration matching is not lookup, because these friend
function names are not visible for normal lookup purposes.
Recommendations
My recommendations for issue 138 (whether using-directives are used
when looking for a prior declaration) are:
-
Using-directives should not be used when looking for a friend function.
-
Using-directives should not be used when looking for a friend class.
-
Using-directives should be used when looking for the name referenced in
a non-friend elaborated type specifier.
Rationale
Friend functions
As described above, the mechanism used to find a prior declaration of
a function is not normal lookup. The question, then, is whether whatever
mechanism is used should make use of names from using-directives.
When the namespace rules were clarified it was agreed that in a declarator
of a definition that uses a qualified-name the lookup of the final component
of the name (the name of the actual entity being defined) cannot make use of
a using-directive. This is reflected in the rule in 8.3p1:
When the declarator-id is qualified, the declaration shall refer to a
previously declared member of the class or namespace to which the
qualifier refers, and the member shall not have been introduced by a
using-declaration in the scope of the class or namespace nominated by
the nested-name-specifier of the declarator-id.
This rule makes the following code ill-formed:
namespace N {
void f();
}
namespace M {
using namespace N;
}
void M::f(){} // illegal attempt to define N::f
A parallel rule for unqualified names was not added because it was not
thought that such a rule was needed under the belief that an unqualified
declaration always declares the name in a specified scope.
This is not clear in the standard though, so it must be clarified one way
or the other.
If an unqualified name can make use of a using-directive, you can actually
change which function is defined by adding a using-directive.
I believe this is very undesirable and is one of the reasons tht the
rule in 8.3 was added.
namespace N {
namespace M {
void f();
}
using namespace M;
class A {
friend void f(){} // N::f or N::M::f?
};
}
Friend classes vs. elaborated type specifiers
Some may object to the proposed difference in handling of friend class
declarations and other forms of elaborated type specifiers. But the
two kinds of elaborated type specifiers are already handled differently.
A friend class declaration does not look beyond the innermost namespace
scope when looking for a prior declaration while a non-friend elaborated
type specifier considers all scopes. This indicates that a friend
declaration is already more "declaration-like" while other forms
of elaborated type specifiers are more "reference-like".
class A {};
class B {};
namespace N {
struct C {
friend class A; // declares N::A
class B* p; // refers to ::B
};
}
End of document.