Почему C ++ не допускает унаследованную дружбу?


95

Почему дружба по крайней мере необязательно наследуется в C ++? Я понимаю, что транзитивность и рефлексивность запрещены по очевидным причинам (я говорю это только для того, чтобы избежать простых ответов на часто задаваемые вопросы), но отсутствие чего-то в этом роде virtual friend class Foo;меня озадачивает. Кто-нибудь знает историческую подоплеку этого решения? Была ли дружба всего лишь ограниченным приемом, который с тех пор нашел свое применение в нескольких малоизвестных респектабельных целях?

Отредактируйте для пояснения: я говорю о следующем сценарии, а не о том, что дочерние элементы A подвергаются воздействию B или одновременно B и его дочерних элементов. Я также могу представить, как необязательно предоставить доступ к переопределению функций друзей и т. Д.

class A {
  int x;
  friend class B;
};

class B {
  // OK as per friend declaration above.
  void foo(A& a, int n) { a.x = n; }
};

class D : public B { /* can't get in A w/o 'friend class D' declaration. */ };

Принятый ответ: как утверждает Локи , эффект можно более или менее смоделировать, создав защищенные прокси-функции в дружественных базовых классах, поэтому нет строгой необходимости предоставлять дружбу классу или иерархии виртуальных методов. Мне не нравится необходимость в шаблонных прокси (которыми фактически становится база друзей), но я полагаю, что это было сочтено предпочтительнее языкового механизма, который, скорее всего, будет неправильно использоваться большую часть времени. Я думаю, что, вероятно, пора мне купить и прочитать книгу Stroupstrup « Дизайн и эволюция C ++» , которую, как я видел, рекомендует здесь достаточно людей, чтобы лучше понять эти типы вопросов ...

Ответы:


94

Потому что я могу написать Fooи своему другу Bar(таким образом возникают доверительные отношения).

Но доверяю ли я людям, которые пишут классы, производные от Bar?
На самом деле, нет. Поэтому они не должны унаследовать дружбу.

Любое изменение внутреннего представления класса потребует модификации всего, что зависит от этого представления. Таким образом, все члены класса, а также все друзья класса потребуют модификации.

Следовательно, если внутреннее представление Foo изменено, то оно Barтакже должно быть изменено (потому что дружба тесно связана Barс Foo). Если бы дружба была унаследована, то весь производный класс Barтакже был бы жестко привязан Fooи, следовательно, потребовал бы модификации, если Fooвнутреннее представление изменилось. Но я не знаю производных типов (и я не должен. Они могут даже быть разработаны разными компаниями и т. Д.). Таким образом, я не смог бы изменить, Fooпоскольку это привело бы к критическим изменениям в базе кода (поскольку я не мог изменить весь производный класс Bar).

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

Примечание: дочерний элемент Barможет получить доступ Fooс помощью Bar, просто сделайте метод Barзащищенным. Затем дочерний Barэлемент может получить доступ к a Foo, вызвав его родительский класс.

Это то, что вы хотите?

class A
{
    int x;
    friend class B;
};

class B
{
    protected:
       // Now children of B can access foo
       void foo(A& a, int n) { a.x = n; }
};

class D : public B
{
    public:
        foo(A& a, int n)
        {
            B::foo(a, n + 5);
        }
};

4
Бинго. Все дело в ограничении ущерба, который вы можете причинить, изменив внутреннее устройство класса.
j_random_hacker

Честно говоря, я действительно думаю об этом в шаблоне «Поверенный-Клиент», где промежуточное звено действует как ограниченный интерфейс для внешних классов, представляя методы оболочки для базового класса с ограниченным доступом. Сказать, что для всех дочерних классов других классов доступен интерфейс, а не конкретный класс, было бы гораздо полезнее, чем система в настоящее время.
Джефф

@Jeff: предоставление внутреннего представления всем дочерним элементам класса приведет к тому, что код станет неизменяемым (он также фактически нарушает инкапсуляцию, поскольку любой, кто хочет получить доступ к внутренним членам, должен только унаследовать от Bar, даже если они на самом деле не являются Bar ).
Мартин Йорк

@Martin: Верно, в этой схеме база друзей может использоваться для доступа к классу друзей, что может быть простым нарушением инкапсуляции во многих (если не в большинстве) случаев. Однако в ситуациях, когда база друзей является абстрактным классом, любой производный класс будет вынужден реализовать собственный взаимный интерфейс. Я не уверен, будет ли класс «самозванец» в этом сценарии рассматриваться как нарушающий инкапсуляцию или нарушающий контракт интерфейса, если он не пытался честно выполнять свою заявленную роль должным образом.
Джефф

@Martin: Верно, это тот эффект, который я хочу и иногда уже использую, где A на самом деле является взаимно дружественным интерфейсом для некоторого класса ограниченного доступа Z. Общая жалоба на обычную идиому поверенного-клиента заключается в том, что класс интерфейса A должен иметь шаблонные обертки вызова для Z, и для расширения доступа к подклассам шаблон A должен быть по существу дублирован в каждом базовом классе, таком как B. Интерфейс обычно выражает, какие функции модуль хочет предложить, а не какие функции в других. хочет использовать себя.
Джефф

48

Почему дружба по крайней мере необязательно наследуется в C ++?

Я думаю, что ответ на ваш первый вопрос заключается в следующем: «Имеют ли друзья вашего отца доступ к вашим интимным комнатам?»


36
Честно говоря, этот вопрос вызывает тревогу о вашем отце. . .
iheanyi

3
В чем смысл этого ответа? в лучшем случае сомнительный, хотя, возможно, беззаботный комментарий
DeveloperChris

11

Класс с друзьями может раскрыть своего друга через функции доступа, а затем предоставить доступ через них.

class stingy {
    int pennies;
    friend class hot_girl;
};

class hot_girl {
public:
    stingy *bf;

    int &get_cash( stingy &x = *bf ) { return x.pennies; }
};

class moocher {
public: // moocher can access stingy's pennies despite not being a friend
    int &get_cash( hot_girl &x ) { return x.get_cash(); }
};

Это обеспечивает более тонкий контроль, чем необязательная транзитивность. Например, get_cashможет быть protectedили может применяться протокол доступа с ограниченным временем выполнения.


@Hector: Голосование за рефакторинг! ;)
Александр Шукаев

8

Стандарт C ++, раздел 11.4 / 8

Дружба не является ни наследственной, ни переходной.

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


2
Скажем, Q "друзья" A, и B происходит от A. Если B наследует дружбу от A, то, поскольку B является типом A, технически это A, который имеет доступ к частным лицам Q. Так что это не отвечает на вопрос с какой-либо практической причиной.
mdenton8

2

Потому что это просто ненужно.

Использование friendключевого слова само по себе подозрительно. С точки зрения сцепления это худшие отношения (намного опережающие наследование и композицию).

Любые изменения во внутреннем устройстве класса могут повлиять на друзей этого класса ... вы действительно хотите неизвестное количество друзей? Вы даже не смогли бы перечислить их, если бы те, кто унаследовал от них, также могли быть друзьями, и вы бы каждый раз подвергались риску взлома кода своих клиентов, что, конечно же, нежелательно.

Я открыто признаю, что для домашних заданий / домашних проектов зависимость часто не рассматривается. В небольших проектах это не имеет значения. Но как только несколько человек работают над одним и тем же проектом, и это разрастается до десятков тысяч строк, вам нужно ограничить влияние изменений.

Это приносит очень простое правило:

Изменение внутренних компонентов класса должно влиять только на сам класс.

Конечно, вы, вероятно, повлияете на его друзей, но здесь два случая:

  • функция без друзей: в любом случае, вероятно, больше функция-член (я думаю std::ostream& operator<<(...), что здесь, которая не является членом чисто случайно из языковых правил
  • класс друга? вам не нужны классы друзей на реальных классах.

Я бы порекомендовал использовать простой метод:

class Example;

class ExampleKey { friend class Example; ExampleKey(); };

class Restricted
{
public:
  void forExampleOnly(int,int,ExampleKey const&);
};

Этот простой Keyшаблон позволяет вам объявить друга (в некотором смысле), фактически не предоставляя ему доступ к вашим внутренним компонентам, тем самым изолируя его от изменений. Кроме того, он позволяет этому другу одалживать свой ключ опекунам (например, детям) при необходимости.


0

Предположение: если класс объявляет в качестве друга какой-то другой класс / функцию, это потому, что этому второму объекту нужен привилегированный доступ к первому. Какая польза от предоставления второму объекту привилегированного доступа к произвольному количеству классов, производных от первого?


2
Если бы класс A хотел предоставить дружбу B и его потомкам, он мог бы избежать обновления своего интерфейса для каждого добавленного подкласса или принуждения B к написанию сквозного шаблона, что, я думаю, является половиной смысла дружбы.
Джефф

@ Джефф: А, тогда я неправильно понял ваш смысл. Я предположил, что вы имели в виду, Bчто у вас будет доступ ко всем классам, унаследованным от A...
Оливер Чарльзуорт,

0

Производный класс может наследовать только то, что является «членом» базы. Объявление друга не является членом класса дружбы.

$ 11.4 / 1- «... Имя друга не входит в область видимости класса, и друг не вызывается с помощью операторов доступа к члену (5.2.5), если он не является членом другого класса».

$ 11.4 - «Кроме того, поскольку базовое предложение дружественного класса не является частью его объявлений членов, базовое предложение дружественного класса не может получить доступ к именам закрытых и защищенных членов из класса, предоставляющего дружбу».

и далее

$ 10.3 / 7- "[Примечание: виртуальный спецификатор подразумевает членство, поэтому виртуальная функция не может быть функцией, не являющейся членом (7.1.2). Также виртуальная функция не может быть статическим членом, поскольку вызов виртуальной функции полагается на конкретный объект для определение того, какую функцию вызывать. Виртуальная функция, объявленная в одном классе, может быть объявлена ​​другом в другом классе.] "

Поскольку «друг» изначально не является членом базового класса, как он может быть унаследован производным классом?


Дружба, хотя и дается через объявления, такие как члены, на самом деле не столько члены, сколько уведомления о том, какие другие классы могут по существу игнорировать классификации видимости для «реальных» членов. В то время как разделы спецификации, которые вы цитируете, объясняют, как язык работает с этими нюансами и формирует поведение в непротиворечивой терминологии, все могло быть создано по-другому, и, к сожалению, ничего из вышеперечисленного не затрагивает сути обоснования.
Джефф

0

Дружественная функция в классе присваивает функции свойство extern. т.е. extern означает, что функция была объявлена ​​и определена где-то вне класса.

Следовательно, это означает, что функция друга не является членом класса. Таким образом, наследование позволяет вам наследовать только свойства класса, а не внешние вещи. А также, если для дружественных функций разрешено наследование, то наследуется сторонний класс.


0

Friend хорош в наследовании, как интерфейс стиля для контейнера. Но для меня, как первый сказал, C ++ не имеет распространяемого наследования

class Thing;

//an interface for Thing container's
struct IThing {
   friend Thing;
   protected:
       int IThing_getData() = 0;
};

//container for thing's
struct MyContainer : public IThing {
    protected: //here is reserved access to Thing
         int IThing_getData() override {...}
};

struct Thing {
    void setYourContainer(IThing* aContainerOfThings) {
        //access to unique function in protected area 
        aContainerOfThings->IThing_getData(); //authorized access
    }
};

struct ChildThing : public Thing {
    void doTest() {
        //here the lack of granularity, you cannot access to the container.
        //to use the container, you must implement all 
        //function in the Thing class
        aContainerOfThings->IThing_getData(); //forbidden access
    }
};

Для меня проблема C ++ заключается в отсутствии очень хорошей детализации для управления всем доступом из любого места для чего угодно:

Friend Thing может стать другом Thing. *, чтобы предоставить доступ ко всем дочерним элементам Thing.

И еще, друг [именованная область] Thing. *, Чтобы предоставить доступ для точного, находятся в классе Container через специальную именованную область для друга.

Хорошо, хватит мечты. Но теперь вы знаете интересное использование слова friend.

В другом порядке вы также можете найти интересным, что все классы дружны с собой. Другими словами, экземпляр класса может
без ограничений вызывать всех членов другого экземпляра с таким же именем:

class Object {
     private:
         void test() {}
     protected:
         void callAnotherTest(Object* anotherObject) {
             //private, but yes you can call test() from 
             //another object instance
             anotherObject)->test(); 
         }
};

0

Простая логика: «У меня есть подруга Джейн. То, что мы подружились вчера, не означает, что все ее друзья мои ».

Мне все еще нужно одобрить эту индивидуальную дружбу, и уровень доверия будет соответствующим.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.