This is an unofficial snapshot of the ISO/IEC JTC1 SC22 WG21 Core Issues List revision 115d. See http://www.open-std.org/jtc1/sc22/wg21/ for the official list.

2024-10-26


95. Elaborated type specifiers referencing names declared in friend decls

Section: _N4868_.9.8.2.3  [namespace.memdef]     Status: NAD     Submitter: John Spicer     Date: 9 Feb 1999

A change was introduced into the language that made names first declared in friend declarations "invisible" to normal lookups until such time that the identifier was declared using a non-friend declaration. This is described in _N4868_.9.8.2.3 [namespace.memdef] paragraph 3 and 11.8.4 [class.friend] paragraph 9 (and perhaps other places).

The standard gives examples of how this all works with friend declarations, but there are some cases with nonfriend elaborated type specifiers for which there are no examples, and which might yield surprising results.

The problem is that an elaborated type specifier is sometimes a declaration and sometimes a reference. The meaning of the following code changes depending on whether or not friend class names are injected (visibly) into the enclosing namespace scope.

    struct A;
    struct B;
    namespace N {
        class X {
            friend struct A;
            friend struct B;
        };
        struct A *p;     // N::A with friend injection, ::A without
        struct B;        // always N::B
    }
Is this the desired behavior, or should all elaborated type specifiers (and not just those of the form "class-key identifier;") have the effect of finding previously declared "invisible" names and making them visible?

Mike Miller: That's not how I would categorize the effect of "struct B;". That declaration introduces the name "B" into namespace N in exactly the same fashion as if the friend declaration did not exist. The preceding friend declaration simply stated that, if a class N::B were ever defined, it would have friendly access to the members of N::X. In other words, the lookups in both "struct A*..." and "struct B;" ignore the friend declarations.

(The standard is schizophrenic on the issue of whether such friend declarations introduce names into the enclosing namespace. 6.4 [basic.scope] paragraph 4 says,

while 6.4.2 [basic.scope.pdecl] paragraph 6 says exactly the opposite: Both of these are just notes; the normative text doesn't commit itself either way, just stating that the name is not found until actually declared in the enclosing namespace scope. I prefer the latter description; I think it makes the behavior you're describing a lot clearer and easier to understand.)

John Spicer: The previous declaration of B is not completely ignored though, because certainly changing "friend struct B;" to "friend union B;" would result in an error when B was later redeclared as a struct, wouldn't it?

Bill Gibbons: Right. I think the intent was to model this after the existing rule for local declarations of functions (which dates back to C), where the declaration is introduced into the enclosing scope but the name is not. Getting this right requires being somewhat more rigorous about things like the ODR because there may be declaration clashes even when there are no name clashes. I suspect that the standard gets this right in most places but I would expect there to be a few that are still wrong, in addition to the one Mike pointed out.

Mike Miller: Regarding would result in an error when B was later redeclared

I don't see any reason why it should. The restriction that the class-key must agree is found in 9.2.9.5 [dcl.type.elab] and is predicated on having found a matching declaration in a lookup according to 6.5.6 [basic.lookup.elab] . Since a lookup of a name declared only (up to that point) in a friend declaration does not find that name (regardless of whether you subscribe to the "does-not-introduce" or "introduces-invisibly" school of thought), there can't possibly be a mismatch.

I don't think that the Standard's necessarily broken here. There is no requirement that a class declared in a friend declaration ever be defined. Explicitly putting an incompatible declaration into the namespace where that friend class would have been defined is, to me, just making it impossible to define — which is no problem, since it didn't have to be defined anyway. The only error would occur if the same-named but unbefriended class attempted to use the nonexisting grant of friendship, which would result in an access violation.

(BTW, I couldn't find anything in the Standard that forbids defining a class with a mismatched class-key, only using one in an elaborated-type-specifier. Is this a hole that needs to be filled?)

John Spicer: This is what 9.2.9.5 [dcl.type.elab] paragraph 3 says:

The latter part of this paragraph (beginning "This rule also applies...") is somewhat murky to me, but I think it could be interpreted to say that

            class B;
            union B {};
and
            union B {};
            class B;
are both invalid. I think this paragraph is intended to say that. I'm not so sure it actually does say that, though.

Mike Miller: Regarding I think the intent was to model this after the existing rule for local declarations of functions (which dates back to C)

Actually, that's not the C (1989) rule. To quote the Rationale from X3.159-1989:

Regarding Getting this right requires being somewhat more rigorous

Yes, I think if this is to be made illegal, it would have to be done with the ODR; the name-lookup-based current rules clearly (IMHO) don't apply. (Although to be fair, the [non-normative] note in 6.4 [basic.scope] paragraph 4 sounds as if it expects friend invisible injection to trigger the multiple-declaration provisions of that paragraph; it's just that there's no normative text implementing that expectation.)

Bill Gibbons: Nor does the ODR currently disallow:

    translation unit #1    struct A;

    translation unit #2    union A;
since it only refers to class definitions, not declarations.

But the obvious form of the missing rule (all declarations of a class within a program must have compatible struct/class/union keys) would also answer the original question.

The declarations need not be visible. For example:

    translation unit #1    int f() { return 0; }

    translation unit #2:   void g() {
                               extern long f();
                           }
is ill-formed even though the second "f" is not a visible declaration.

Rationale (10/99): The main issue (differing behavior of standalone and embedded elaborated-type-specifiers) is as the Committee intended. The remaining questions mentioned in the discussion may be addressed in dealing with related issues.

(See also issues 136, 138, 139, 143, 165, and 166.)