c++language-lawyerfriendfriend-functionlanguage-history

What is the point of the complicated scoping rules for friend declarations?


I recently discovered that friend declarations scoping follows extremely peculiar rules - if you have a friend declaration (definition) for a function or a class that is not already declared, it is automatically declared (defined) in the immediately enclosing namespace, but it is invisible to non-qualified and qualified lookup; however, friend function declarations remain visible through argument-dependent lookup.

struct M {
    friend void foo();
    friend void bar(M);
};

void baz() {
    foo();    // error, unqualified lookup cannot find it
    ::foo();  // error, qualified lookup cannot find it
    bar(M()); // ok, thanks to ADL magic
}

If you look at the standard (see linked answer), they went to significant lengths to enable this eccentric behavior, adding a specific exception in qualified/non qualified lookup with complex rules. The end result looks to me extremely confusing1, with yet-another-corner case to add to implementations. As either

seem simpler to implement, to specify and, most importantly, to understand, I wonder: why did they bother with this mess? What use cases were they trying to cover? What breaks under any of those simpler rules (in particular the second one, which is the most similar to the existing behavior)?


  1. For example, in this particular case

    struct M {
       friend class N;
    };
    N *foo;
    typedef int N;
    

    you get comically schizophrenic error messages

    <source>:4:1: error: 'N' does not name a type
     N *foo;
     ^
    <source>:5:13: error: conflicting declaration 'typedef int N'
     typedef int N;
                 ^
    <source>:2:17: note: previous declaration as 'class N'
        friend class N;
                     ^
    

    where the compiler first claims that there's no such a thing as N, but immediately stops playing dumb when you try to provide a conflicting declaration.


Solution

  • Well, for answering that, you have to look at another major feature of C++: Templates.

    Consider a template such as this:

    template <class T>
    struct magic {
        friend bool do_magic(T*) { return true; }
    };
    

    Used in code like this:

    bool do_magic(void*) { return false; }
    
    int main() {
        return do_magic((int*)0);
    }
    

    Will the exit-code be 0 or 1?

    Well, it depends on whether magic was ever instantiated with int anywhere observable.
    At least it would, if friend-functions only declared inline would be found by ordinary lookup-rules.
    And you can't break that conundrum by just injecting everything possible, as templates can be specialized.

    That was the case for a time, but was outlawed as "too magic", and "too ill-defined".

    There were additional problems with name injection, as it wasn't nearly as well-defined as hoped for. See N0777: An Alternative to Name Injection from Templates for more.