В чем смысл сложных правил определения правил для объявлений друзей?

Недавно я обнаружил, что обзор деклараций друзей следует чрезвычайно специфическим правилам - если у вас есть объявление friend (определение) для функции или класса, который еще не объявлен, он автоматически объявляется (определяется) в непосредственно вложенном пространстве имен, но он невидим к неквалифицированному и квалифицированному поиску; однако объявления функций друга остаются видимыми через зависящий от аргумента поиск.

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
}

Если вы посмотрите на стандарт (см. Связанный ответ), они пошли на значительную длину, чтобы включить это эксцентричное поведение, добавив конкретное исключение в квалифицированный/не квалифицированный поиск со сложными правилами. Конечный результат выглядит очень запутанным 1 с еще одним аргументом в углу, чтобы добавить к реализациям. Как либо

  • требуя, чтобы объявления friend ссылались на существующие имена, период; или же
  • позволяя им объявлять материал таким, какой он есть сейчас, но без изменения обычного поиска имен (поэтому такие имена становятся видимыми, как если бы они были объявлены "нормально" в охватывающем пространстве имен)

кажется, проще реализовать, указать и, самое главное, понять, интересно, почему они беспокоились об этом беспорядке? Какие варианты использования они пытались охватить? Что ломается под любым из этих более простых правил (в частности, второе, наиболее похожее на существующее поведение)?


  1. Например, в данном конкретном случае

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

    вы получаете комически шизофренические сообщения об ошибках

    <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;
                     ^
    

    где компилятор сначала утверждает, что нет такой вещи, как N, но сразу же перестает играть глупо, когда вы пытаетесь представить противоречивую декларацию.

Ответ 1

Что ж, для ответа на это вам нужно взглянуть на еще одну важную особенность C++: Шаблоны.

Рассмотрим такой шаблон:

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

Используется в коде:

bool do_magic(void*) { return false; }

int main() {
    return do_magic((int*)0);
}

Будет ли код выхода 0 или 1?

Ну, это зависит от того, была ли magic когда-либо созданной с int где-либо наблюдаемой.
По крайней мере, это было бы, если friend -functions только декларировали рядный будет найдено обычными подстановками-правила.
И вы не можете нарушить эту загадку, просто вложив все возможное, поскольку шаблоны могут быть специализированными.

Так было на какое-то время, но было объявлено вне закона как "слишком волшебное" и "слишком плохо определено".

Появились дополнительные проблемы с инъекцией имени, поскольку это было не так точно определено, как надеялись. См. N0777: Альтернатива для ввода имени из шаблонов для получения дополнительной информации.