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

1

11

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

  • requiring friend declarations to refer to existing names, period; or
  • allowing them to declare stuff as it is now, but without altering the ordinary names lookup (so, such names become visible as if declared "normally" in the enclosing namespace)

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.

Neopythagoreanism answered 31/8, 2018 at 20:51 Comment(10)
I'm not really sure what you're looking for here. Like just... why only visible to ADL?Analog
@Barry: I'm asking why they bothered with this mess instead e.g. of just leaving them visible to all lookups, without strange rules that create those "phantom" names. Several paragraphs less in the standard, several thousands less neurons wasted remembering special cases, simpler compiler implementations. My (possibly wrong) implicit assumption is that if they went to this length to create these bizarre half-visible entities there's some particular use case they were aiming for, but I cannot think of any.Neopythagoreanism
My best guess is that they didn't want declarations in one scope introduce names in another scope.Wacker
@Rakete1111: but they do introduce them; it's just that qualified/non-qualified lookup cannot find them, but they are there, and you become painfully aware of that if you try to provide incompatible declarations in the enclosing namespace. Also, if they really wanted to avoid that they could have just required friend declarations to refer to already existing names.Neopythagoreanism
@MatteoItalia I just found the 1996 (!) paper that has the proposed wording to disallow this. Now I just need to find the rationale...Wacker
There is also open-std.org/jtc1/sc22/wg21/docs/papers/1995/N0777.pdfAldas
@matteo: Even though I agree with you on the strangeness about friend-ness ,I think the error-messages you supplied are perfectly logical in regard to the friend-rules.Biquarterly
@engf-010: of course, but imagine for a moment being like me (and I suspect at least 90% C++ programmers) up to yesterday, not knowing about these crazy rules; you'd think the compiler had gone mad.Neopythagoreanism
I wonder if this is at all related to the strange magic of friend functions of class templates. You can use this technique to define a free function that isn't actually a template itself, but rather behaves the same way as a non template member of a class template. The first of your suggestions would break this trick, and the second suggestion is creating a wider interface than necessary for what the trick is achieving.Operate
@NirFriedman indeed it's related; read the paper linked in the answer for the gory details.Neopythagoreanism
P
12

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.

Partridgeberry answered 31/8, 2018 at 21:18 Comment(6)
Why should it depend on instantiation of magic? Why wouldn't that class definition just inject a function template into the enclosing namespace?Borreri
@Brian Why would it be a function template?Wacker
Woa that's indeed a compelling explanation; honestly the "clean" thing IMHO would have just been banning the name injection altogether (i.e. the first alternative proposal in my question), but I suspect that it predates templates, and by the time they found this problem they couldn't break compatibility?Neopythagoreanism
... ah no, reading the paper the use case of injection is clear, and is actually needed exactly for templates. TIL!Neopythagoreanism
So, essentially the paper proposed to disable completely injection for templates (hence it was probably already existent and used for "regular" classes), and to clarify the template type deduction rules to allow a trick using default template arguments to work to cover the use cases of injected friend functions for templates. Insightful read, it would be interesting to understand how/why later this was rejected in favor of the current wording.Neopythagoreanism
@MatteoItalia Well, there's the problem that it would have disabled type-deducation for any template-argument with a default. template <class T, class Alloc = std::allocator<std::decay_t<T>> auto make_thing(T&& t, Alloc a = Alloc()); and the like would be far less useful than before. At least if I understood it right.Partridgeberry

© 2022 - 2024 — McMap. All rights reserved.