Course program← Программирование на языке C++
1Введение в язык C++
2Как выполняются программы на C++
3Структуры и классы
4Объектно-ориентированное программирование
5Дополнительные возможности C++
6Шаблоны
7Методические материалы

Ключевое слово friend

This lesson is not translated yet – the Russian original is below.

private закрывает доступ всем, кроме методов класса. Иногда это слишком строго: есть код, который логически принадлежит классу, но методом быть не может. friend – способ сделать в стене именно ту дверь, которая нужна.

Синтаксис

class Account {
public:
    Account(int balance) : balance_(balance) {}

    friend class Auditor;                                     // весь класс
    friend void PrintBalance(const Account& a);               // одна функция
    friend std::ostream& operator<<(std::ostream&, const Account&);

private:
    int balance_;
};

void PrintBalance(const Account& a) {
    std::cout << a.balance_;      // доступ есть
}

Ключевое здесь – дружбу выдаёт тот, к кому получают доступ. Снаружи назначить себя другом чужого класса нельзя, так что инкапсуляция не обходится, а расширяется владельцем класса сознательно.

Объявление friend не является объявлением члена – модификатор доступа, в секции которого оно написано, ни на что не влияет. Писать его принято рядом с публичным интерфейсом.

Свойства дружбы

Три правила, которые нужно помнить:

Не взаимна. A пустил B – значит, B видит приватное в A, но не наоборот.

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

Не транзитивна. Друг вашего друга – не ваш друг.

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

Зачем это нужно на практике

Операторы, которые не могут быть методами

Главный случай. У метода левый операнд – всегда *this, поэтому симметричные операторы и вывод в поток методами не выражаются:

class Rational {
public:
    Rational(int num, int den) : num_(num), den_(den) {}

    friend Rational operator+(const Rational& a, const Rational& b);
    friend std::ostream& operator<<(std::ostream& out, const Rational& r);

private:
    int num_;
    int den_;
};

Rational operator+(const Rational& a, const Rational& b) {
    return Rational(a.num_ * b.den_ + b.num_ * a.den_, a.den_ * b.den_);
}

std::ostream& operator<<(std::ostream& out, const Rational& r) {
    return out << r.num_ << '/' << r.den_;
}

Свободная функция здесь нужна ещё и для симметрии преобразований: 1 + r работает, а с методом не работало бы.

Тесно связанные классы

Два класса, составляющие одну абстракцию: контейнер и его итератор, узел и список.

class List;

class Node {
    friend class List;      // список знает про узлы всё

    int value_;
    Node* next_;
};

Альтернатива – публичный интерфейс у Node, но тогда во внутренности узла сможет залезть кто угодно. Дружба здесь уже, а не шире.

Тесты

Спорный, но распространённый приём: дать тесту доступ к внутреннему состоянию.

class Cache {
    friend class CacheTest;   // тест проверяет инварианты изнутри
};

Аргумент против: тест, знающий внутренности, ломается при каждом рефакторинге и перестаёт проверять то, ради чего написан, – наблюдаемое поведение. Аргумент за: инвариант, который снаружи не наблюдаем, иначе не проверить. Решать по ситуации, но по умолчанию лучше тестировать через публичный интерфейс.

Друг или геттер

Дружба – не единственный способ дать доступ. Часто уместнее публичные методы чтения:

class Rational {
public:
    int Num() const { return num_; }
    int Den() const { return den_; }

private:
    int num_;
    int den_;
};

std::ostream& operator<<(std::ostream& out, const Rational& r) {
    return out << r.Num() << '/' << r.Den();   // friend не нужен
}
friend Публичные геттеры
Кто видит внутренности перечисленные поимённо все
Список зависимых от устройства класса известен и ограничен неизвестен
Интерфейс класса не растёт растёт

Правило: если геттеры и так нужны пользователям класса – делайте геттеры, friend не нужен. Если доступ требуется только одной функции и открывать его всем не хочется – friend честнее, чем публичный метод «для служебного пользования».

Иногда встречается мнение, что friend «ломает инкапсуляцию». Это неточно: дружба перечисляет тех, кто зависит от устройства класса, поимённо и в самом классе. Публичный геттер такого списка не даёт – зависеть от него может кто угодно. С точки зрения «что придётся править при изменении полей» friend зачастую строже.

Определение внутри класса

friend-функцию можно определить прямо в теле класса:

class Rational {
public:
    friend Rational operator+(const Rational& a, const Rational& b) {
        return Rational(a.num_ * b.den_ + b.num_ * a.den_, a.den_ * b.den_);
    }

private:
    int num_;
    int den_;
};

Функция при этом остаётся свободной – методом она не становится, вызвать её как r.operator+(x) нельзя. Особенность в том, что найти её можно только поиском, зависящим от аргументов (ADL): по имени вне контекста этого класса она не видна. Приём известен как «скрытый друг» и в современном коде считается хорошим тоном – он не засоряет пространство имён и ускоряет разрешение перегрузок.