Синтаксис си/с++

Дружественные функции и классы (friend)

Введение

Статья 535 уже использовала friend для перегрузки operator<< без подробного объяснения — в этой статье разберём friend систематически: что это означает с точки зрения инкапсуляции (статья 531), и когда его использование действительно оправдано, а когда говорит о проблеме в архитектуре классов.

Концепция

friend объявляет, что указанная функция (или весь другой класс) получает доступ к приватным и защищённым членам класса, в котором это объявление сделано — это единственный, явный, контролируемый способ «пробить» обычную инкапсуляцию (статья 531) для конкретных, явно перечисленных внешних функций/классов, не открывая доступ для произвольного, неконтролируемого кода извне. Дружба не является взаимной автоматически (если класс A объявляет класс B своим другом, это не означает, что B автоматически предоставляет A такой же доступ к своим собственным приватным членам) и не наследуется (производный класс не наследует «дружеские» отношения базового класса автоматически).

Пример кода: Дружественные функции и классы (friend)

#include <iostream>

class BankAccount {
public:
    BankAccount(double balance) : m_balance(balance) {}

    // friend-функция — НЕ метод класса, но имеет доступ к ПРИВАТНЫМ членам
    friend void auditAccount(const BankAccount &account);

private:
    double m_balance;
};

void auditAccount(const BankAccount &account) // обычная свободная функция, не метод класса
{
    // Доступ к ПРИВАТНОМУ m_balance благодаря объявлению friend внутри BankAccount
    std::cout << "Аудит: баланс счёта составляет " << account.m_balance << std::endl;
}

int main()
{
    BankAccount account(1000.0);
    auditAccount(account); // вызов как обычной свободной функции, НЕ через account.auditAccount()
    return 0;
}
// friend class — целый другой класс получает доступ к приватным членам
class Engine {
private:
    int m_horsepower = 300;
    friend class Car; // ВЕСЬ класс Car получает доступ к приватным членам Engine
};

class Car {
public:
    void printEngineInfo(const Engine &engine)
    {
        std::cout << "Лошадиных сил: " << engine.m_horsepower << std::endl; // доступ к приватному полю Engine
    }
};

Пояснения к коду

friend void auditAccount(const BankAccount &account); внутри BankAccount показывает декларацию дружбы для конкретной, отдельной функции — несмотря на то, что auditAccount не является методом BankAccount (вызывается как обычная свободная функция auditAccount(account), а не account.auditAccount()), она получает полный доступ к приватным полям BankAccount, что и было явной целью объявления friend. friend class Car; внутри Engine показывает более широкую форму дружбы — весь класс Car (включая абсолютно все его методы) получает доступ к приватным членам Engine, что удобно, когда несколько разных методов одного класса должны иметь такой доступ, избегая необходимости объявлять каждый такой метод как friend индивидуально.

Подводные камни

  • Избыточное использование friend как способа «обойти» проблемы плохо спроектированного публичного интерфейса класса, вместо реального пересмотра и улучшения этого интерфейса — если для решения конкретной задачи регулярно требуется доступ к приватным деталям класса через friend извне его собственных методов, это часто сигнализирует о том, что публичный интерфейс класса недостаточно полон или хорошо продуман, и более архитектурно правильное решение — расширить публичный интерфейс необходимыми методами, а не наращивать число friend-деклараций, постепенно размывающих границы реальной инкапсуляции класса.
  • Заблуждение о том, что дружба является взаимной или наследуемой — как явно отмечено в концепции, объявление класса A другом класса B не даёт B аналогичного доступа к приватным членам A (это требует ОТДЕЛЬНОГО, явного объявления дружбы в обратную сторону, если она реально нужна), и производный от A класс не наследует автоматически статус «друга» класса A — оба этих неверных предположения о работе friend — частый источник путаницы для разработчиков, не до конца разобравшихся с точной семантикой этого механизма.
  • Чрезмерно широкое использование friend class, предоставляющее доступ ко ВСЕМ приватным членам, когда реально нужен доступ лишь к одному конкретному полю/методу — это нарушает принцип наименьших привилегий (аналогично статье 434 SQL-цикла, но применительно к доступу внутри кода, а не к правам в БД), излишне расширяя поверхность потенциального, нежелательного взаимодействия между классами, и там, где практически возможно, более точечное предоставление доступа (через friend для конкретной функции, а не целого класса, или через специально предназначенный, более узкий публичный метод) предпочтительнее широкой дружбы целого класса.
  • Использование friend как замены реальной, продуманной архитектуры взаимодействия между классами, особенно в случаях, разрастающихся до сложной сети взаимных дружественных отношений между множеством классов — большое количество перекрёстных friend-объявлений между многими классами существенно усложняет понимание реальных границ инкапсуляции и зависимостей в кодовой базе, и хотя friend является легитимным, иногда необходимым инструментом C++ (особенно для случаев, аналогичных перегрузке операторов ввода-вывода статьи 535, где альтернативы менее элегантны), его разрастающееся, бесконтрольное использование часто говорит о необходимости пересмотра общей архитектуры взаимодействия классов.