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

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:
  1. Using-directives should not be used when looking for a friend function.
  2. Using-directives should not be used when looking for a friend class.
  3. 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.