Друзья / FAQ C++

Добавлено 6 ноября 2020 в 22:50

Что такое friend?

Это то, что позволяет вашему классу предоставлять доступ к себе другому классу или функции.

Друзья могут быть функциями или другими классами. Класс предоставляет своим друзьям права доступа. Обычно разработчик имеет политический и технический контроль и над друзьями, и над функциями-членами класса (в противном случае вам может потребоваться разрешение от владельца других частей, если вы хотите обновить свой собственный класс).


Нарушают ли друзья инкапсуляцию?

Нет! При правильном использовании они улучшают инкапсуляцию.

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

class X {
  int i;
public:
  void m();           // предоставляет доступ X::m()
  friend void f(X&);  // предоставляет доступ f(X&)
  // ...
};

void X::m() { i++;    /* X::m() может получить доступ к X::i */ }
void f(X& x) { x.i++; /* f(X&) может получить доступ к X::i */ }

Описание модели защиты C++ смотрите в D&E (раздел 2.10) и TC++PL (разделы 11.5, 15.3 и C.11).

Часто бывает необходимо разделить класс пополам, если у этих двух половин будет разное количество экземпляров или разное время жизни. В этих случаях двум половинкам обычно требуется прямой доступ друг к другу (две половины раньше находились в одном классе, поэтому вы не увеличили объем кода, которому требуется прямой доступ к структуре данных; вы просто перетасовали код на два класса вместо одного). Самый безопасный способ реализовать это – подружить эти две половинки.

Если вы используете друзей, только как описано выше, то, что было private, так и останется private. Люди, которые этого не понимают, часто прилагают наивные усилия, чтобы избежать использования дружбы в ситуациях, подобных описанным выше, и часто фактически разрушают инкапсуляцию. Они либо используют публичные данные (абсурд!), либо делают данные доступными между этими половинами через публичные функции-члены get() и set(). Наличие публичных функций-членов get() и set() для частных данных – это нормально, только когда эти частные данные «имеют смысл» извне класса (с точки зрения пользователя). Во многих случаях эти функции-члены get()/set() почти так же плохи, как и общедоступные данные: они скрывают (только) имя частных данных, но не скрывают само существование частных данных.

Точно так же, если вы используете дружественные функции как синтаксический вариант функций, доступных в секции public класса, они не нарушают инкапсуляцию больше, чем ее нарушает функция-член. Другими словами, друзья класса не нарушают барьер инкапсуляции: вместе с функциями-членами класса они являются барьером инкапсуляции.

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


Какие преимущества/недостатки есть у использования дружественных функций?

Они предоставляют некоторую свободу в вариантах проектирования интерфейса.

Функции-члены и дружественные функции имеют одинаковые привилегии (наделены ими на 100%). Основное отличие состоит в том, что дружественная функция вызывается как f(x), а функция-член – как x.f(). Таким образом, возможность выбора между функциями-членами (x.f()) и дружественными функциями (f(x)) позволяет разработчику выбирать синтаксис, который считается наиболее читаемым, что снижает затраты на поддержку.

Основным недостатком дружественных функций является то, что они требуют дополнительной строки кода, когда вам нужна динамическая привязка. Чтобы получить эффект virtual friend, дружественная функция должна вызывать скрытую (обычно защищенную) виртуальную функцию-член. Это называется идиомой виртуальной дружественной функции (Virtual Friend Function Idiom). Например:

class Base {
public:
  friend void f(Base& b);
  // ...
protected:
  virtual void do_f();
  // ...
};

inline void f(Base& b)
{
  b.do_f();
}

class Derived : public Base {
public:
  // ...
protected:
  virtual void do_f();  // "Переопределить" поведение f(Base& b)
  // ...
};

void userCode(Base& b)
{
  f(b);
}

Выражение f(b) в userCode(Base&) вызовет метод b.do_f(), который является виртуальным. Это означает, что Derived::do_f() получит управление, если b на самом деле является объектом класса Derived. Обратите внимание, что Derived переопределяет поведение защищенной виртуальной функции-члена do_f(); у него нет собственного варианта дружественной функции друга, f(Base&).


Что значит «дружба не передается по наследству, не является переходящей или взаимной»?

Тот факт, что я предоставляю вам дружественный доступ ко мне, не дает автоматически доступ ко мне вашим детям, не предоставляет автоматически доступ ко мне вашим друзьям и не предоставляет мне автоматически доступ к вам.

  • Я не обязательно доверяю детям своих друзей. Привилегии дружбы не передаются по наследству. Производные дружественные классы – не обязательно друзья. Если класс Fred объявляет, что класс Base является другом, классы, производные от Base, не имеют никаких автоматических специальных прав доступа к объектам Fred.
  • Я не обязательно доверяю друзьям своих друзей. Привилегии дружбы не передаваемы. Друг друга – не обязательно друг. Если класс Fred объявляет своим другом класс Wilma, а класс Wilma объявляет своим другом класс Betty, класс Betty не обязательно имеет какие-либо особые права доступа к объектам Fred.
  • Вы не обязательно доверяете мне только потому, что я объявляю вас своим другом. Привилегии дружбы не взаимны. Если класс Fred объявляет, что класс Wilma является ему другом, объекты Wilma имеют особый доступ к объектам Fred, но объекты Fred не имеют автоматически особого доступа к объектам Wilma.

Что мне лучше объявлять в своем классе, функцию-член или дружественную функцию?

Используйте функцию-член, когда можете, и дружественную функцию, когда вам нужно.

Иногда друзья синтаксически лучше (например, в классе Fred дружественные функции позволяют параметру Fred быть вторым, в то время как функции-члены требуют, чтобы он был первым). Еще одно хорошее применение дружественных функций – это двоичные инфиксные арифметические операторы. Например, aComplex + aComplex должен быть определен как друг, а не как член, если вы хотите разрешить aFloat + aComplex (функции-члены не позволяют продвигать левый аргумент, так как это изменит класс объекта, который является получателем вызова функции-члена).

В остальных случаях выбирайте функцию-член вместо дружественной функции.

Оригинал статьи:

Теги

C++ / CppFAQfriendВысокоуровневые языки программированияЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.