Друзья / FAQ C++
Что такое 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
(функции-члены не позволяют продвигать левый аргумент, так как это изменит класс объекта, который является получателем вызова функции-члена).
В остальных случаях выбирайте функцию-член вместо дружественной функции.