Когда следует использовать слово «друг» в C++?

Я читал FAQ по C++, и мне было интересно узнать об friendобъявлении. Я лично никогда им не пользовался, но мне интересно изучать язык.

Какой хороший пример использования friend?


Почитав FAQ еще немного, мне нравится идея << >> перегрузки оператора и добавления в друзья этих классов. Однако я не уверен, как это не нарушает инкапсуляцию. Когда эти исключения могут оставаться в рамках строгости ООП?

Ответов (25)

Решение

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

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

На ответ;

Спецификатор friend разрешает указанному классу доступ к защищенным данным или функциям внутри класса, выполняющего оператор friend. Например, в приведенном ниже коде любой может спросить ребенка, как его имя, но только мать и ребенок могут изменить имя.

Вы можете продолжить этот простой пример, рассмотрев более сложный класс, такой как Window. Вполне вероятно, что в Window будет много элементов функций / данных, которые не должны быть общедоступными, но НЕОБХОДИМЫ для связанного класса, такого как WindowManager.

class Child
{
//Mother class members can access the private parts of class Child.
friend class Mother;

public:

  string name( void );

protected:

  void setName( string newName );
};

При реализации древовидных алгоритмов для класса в коде фреймворка, который нам предоставил профессор, был древовидный класс как друг класса узла.

На самом деле это не приносит никакой пользы, кроме как позволить вам получить доступ к переменной-члену без использования функции настройки.

Friend пригодится, когда вы создаете контейнер и хотите реализовать итератор для этого класса.

У нас возникла интересная проблема в компании, в которой я раньше работал, где мы использовали друга для достойного воздействия. Я работал в нашем отделе фреймворков, мы создали базовую систему уровня движка поверх нашей собственной ОС. Внутри у нас была структура классов:

         Game
        /    \
 TwoPlayer  SinglePlayer

Все эти классы были частью фреймворка и поддерживались нашей командой. Игры, выпускаемые компанией, были созданы на основе этой платформы, разработанной одним из детей Games. Проблема заключалась в том, что у Game были интерфейсы к различным вещам, к которым требовался доступ SinglePlayer и TwoPlayer, но которые мы не хотели открывать за пределами классов фреймворка. Решение заключалось в том, чтобы сделать эти интерфейсы приватными и разрешить доступ к ним TwoPlayer и SinglePlayer через дружбу.

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

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

Point p;
cout << p;

Однако для этого может потребоваться доступ к личным данным Point, поэтому мы определяем перегруженный оператор

friend ostream& operator<<(ostream& output, const Point& p);

Однако есть очевидные последствия инкапсуляции. Во-первых, теперь класс или функция друга имеют полный доступ ко ВСЕМ членам класса, даже к тем, которые не относятся к его потребностям. Во-вторых, реализации класса и друга теперь связаны до такой степени, что внутреннее изменение в классе может сломать друга.

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

Чтобы добиться того же, чего хотят достичь «друзья», но без нарушения инкапсуляции, можно сделать следующее:

class A
{
public:
    void need_your_data(B & myBuddy)
    {
        myBuddy.take_this_name(name_);
    }
private:
    string name_;
};

class B
{
public:
    void print_buddy_name(A & myBuddy)
    {
        myBuddy.need_your_data(*this);
    }
    void take_this_name(const string & name)
    {
        cout << name;
    }
}; 

Инкапсуляция не нарушена, класс B не имеет доступа к внутренней реализации в A, но результат такой же, как если бы мы объявили B другом A. Компилятор оптимизирует вызовы функций, поэтому это приведет к тому же инструкции как прямой доступ.

Я думаю, что использование слова «друг» - это просто ярлык с очевидной пользой, но с определенной стоимостью.

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

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

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

Проблема с первым вариантом заключается в том, что это может быть ОЧЕНЬ много средств доступа, что (немного) медленнее, чем прямой доступ к переменным, сложнее для чтения и громоздко. Проблема со вторым подходом заключается в том, что вы полностью нарушаете инкапсуляцию.

Было бы неплохо, если бы мы могли определить внешнюю функцию, которая все еще могла бы получать доступ к закрытым членам класса. Мы можем сделать это с помощью friend ключевого слова:

class Beer {
public:
    friend bool equal(Beer a, Beer b);
private:
    // ...
};

Метод в equal(Beer, Beer) настоящее время имеет прямой доступ к a и b «s частных пользователей (которые могут быть char *brand, float percentAlcohol и т.д. Это довольно надуманный пример, вы бы скорее обратиться friend к перегруженным == operator, но мы вернемся к этому.

Несколько замечаний:

  • A friendНЕ является функцией-членом класса
  • Это обычная функция со специальным доступом к закрытым членам класса.
  • Не заменяйте все аксессоры и мутаторы друзьями (вы также можете сделать все public!)
  • Дружба не взаимна
  • Дружба непостоянна
  • Дружба не передается по наследству
  • Или, как объясняется в C++ FAQ : «Просто потому, что я предоставляю вам доступ ко мне для дружбы, это не означает, что ваши дети автоматически получают доступ ко мне, не предоставляют автоматически доступ ко мне вашим друзьям и не предоставляют мне автоматически доступ к вам. . "

Я действительно использую только friends тогда, когда намного сложнее сделать наоборот. В качестве другого примера, функции многого вектора математика часто создаются в friends связи с совместимостью Mat2x2, Mat3x3, Mat4x4, Vec2, Vec3, Vec4 и т.д. И это просто так гораздо проще быть друзьями, а не должен использовать аксессор везде. Как уже указывалось, friend часто бывает полезно при применении к << (действительно удобно для отладки) >> и, возможно, к == оператору, но также может использоваться для чего-то вроде этого:

class Birds {
public:
    friend Birds operator +(Birds, Birds);
private:
    int numberInFlock;
};


Birds operator +(Birds b1, Birds b2) {
    Birds temp;
    temp.numberInFlock = b1.numberInFlock + b2.numberInFlock;
    return temp;
}

Как я уже сказал, я использую не friend очень часто, но время от времени это именно то, что вам нужно. Надеюсь это поможет!

Вы можете использовать дружбу, когда разные классы (не наследующие один от другого) используют закрытые или защищенные члены другого класса.

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

с http://www.cplusplus.com/doc/tutorial/inheritance/ .

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

// friend functions
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle() {}
    Rectangle (int x, int y) : width(x), height(y) {}
    int area() {return width * height;}
    friend Rectangle duplicate (const Rectangle&);
};

Rectangle duplicate (const Rectangle& param)
{
  Rectangle res;
  res.width = param.width*2;
  res.height = param.height*2;
  return res;
}

int main () {
  Rectangle foo;
  Rectangle bar (2,3);
  foo = duplicate (bar);
  cout << foo.area() << '\n';
  return 0;
}

В C++ ключевое слово friend полезно при перегрузке операторов и создании моста.

1.) Ключевое слово Friend в перегрузке оператора:
Пример перегрузки оператора: Допустим, у нас есть класс «Point», который имеет две плавающие переменные
«x» (для координаты x) и «y» (для координаты y). Теперь нам нужно перегрузить "<<" (оператор извлечения), чтобы при вызове "cout << pointobj" он выводил координаты x и y (где pointobj - объект класса Point). Для этого у нас есть два варианта:

   1. Перегрузить функцию "operator << ()" в классе "ostream".
   2. Перегрузить функцию "operator << ()" в классе "Point".
Теперь вариант First не подходит, потому что, если нам нужно снова перегрузить этот оператор для какого-то другого класса, мы должны снова внести изменения в класс «ostream».
Поэтому второй вариант - лучший. Теперь компилятор может вызывать "operator <<()"функцию:

   1. Использование объекта ostream cout.As: cout.operator << (Pointobj) (форма класса ostream). 
2. Вызов без объекта. Как: operator << (cout, Pointobj) (из класса Point).

Потому что мы реализовали перегрузку в классе Point. Итак, чтобы вызвать эту функцию без объекта, мы должны добавить "friend" ключевое слово, потому что мы можем вызвать функцию друга без объекта. Теперь объявление функции будет As:
"friend ostream &operator<<(ostream &cout, Point &pointobj);"

2.) Ключевое слово Friend при создании моста:
предположим, мы должны создать функцию, в которой мы должны получить доступ к закрытому члену двух или более классов (обычно называемых «мостом»). Как это сделать:
чтобы получить доступ к закрытому члену класса, он должен быть членом этого класса. Теперь, чтобы получить доступ к закрытому члену другого класса, каждый класс должен объявить эту функцию как функцию друга. Например: предположим, что существует два класса A и B. Функция "funcBridge()" хочет получить доступ к закрытому члену обоих классов. Затем оба класса следует объявить "funcBridge()" как:
friend return_type funcBridge(A &a_obj, B & b_obj);

Я думаю, это поможет понять ключевое слово друга.

Возможно, я что-то упустил из приведенных выше ответов, но еще одна важная концепция инкапсуляции - это сокрытие реализации. Уменьшение доступа к частным элементам данных (деталям реализации класса) позволяет значительно упростить последующую модификацию кода. Если друг напрямую обращается к личным данным, любые изменения в полях данных реализации (личные данные) нарушают код доступа к этим данным. Использование методов доступа в основном устраняет это. Думаю, довольно важно.

Как говорится в ссылке на объявление друга :

Объявление друга появляется в теле класса и предоставляет функции или другому классу доступ к закрытым и защищенным членам класса, в котором появляется объявление друга.

Напоминаем, что в некоторых ответах есть технические ошибки, в которых говорится, что friend можно посещать только защищенных участников.

Я нашел удобное место для использования доступа друзей: Unittest частных функций.

@roo : инкапсуляция здесь не нарушена, потому что класс определяет, кто может получить доступ к его закрытым членам. Инкапсуляция была бы нарушена, только если бы это могло быть вызвано извне класса, например, если operator << бы вы провозгласили «Я друг класса foo ».

friend заменяет использование public, а не использование private !

Собственно, FAQ по C++ уже отвечает на это .

friend Ключевое слово имеет несколько хороших целей. Вот два варианта использования, которые мне сразу видны:

Определение друга

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

namespace utils {
    class f {
    private:
        typedef int int_type;
        int_type value;

    public:
        // let's assume it doesn't only need .value, but some
        // internal stuff.
        friend f operator+(f const& a, f const& b) {
            // name resolution finds names in class-scope. 
            // int_type is visible here.
            return f(a.value + b.value);
        }

        int getValue() const { return value; }
    };
}

int main() {
    utils::f a, b;
    std::cout << (a + b).getValue(); // valid
}

Частный базовый класс CRTP

Иногда вы обнаруживаете, что политике нужен доступ к производному классу:

// possible policy used for flexible-class.
template<typename Derived>
struct Policy {
    void doSomething() {
        // casting this to Derived* requires us to see that we are a 
        // base-class of Derived.
        some_type const& t = static_cast<Derived*>(this)->getSomething();
    }
};

// note, derived privately
template<template<typename> class SomePolicy>
struct FlexibleClass : private SomePolicy<FlexibleClass> {
    // we derive privately, so the base-class wouldn't notice that, 
    // (even though it's the base itself!), so we need a friend declaration
    // to make the base a friend of us.
    friend class SomePolicy<FlexibleClass>;

    void doStuff() {
         // calls doSomething of the policy
         this->doSomething();
    }

    // will return useful information
    some_type getSomething();
};

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

Вы контролируете права доступа для членов и функций, используя право Private / Protected / Public? Итак, если предположить, что идея каждого из этих трех уровней ясна, тогда должно быть ясно, что мы чего-то упускаем ...

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

Таким образом, друг дает вам гибкость, позволяющую изолировать твердые объекты, но позволяет создать «лазейку» для вещей, которые, по вашему мнению, оправданы.

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

На самом деле это не приносит никакой пользы, кроме как позволить вам получить доступ к переменной-члену без использования функции настройки

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

Канонический пример - перегрузка оператора <<. Другое распространенное использование - предоставить помощнику или классу администратора доступ к вашим внутренним компонентам.

Вот несколько советов о друзьях C++, которые я слышал. Последний особенно запомнился.

  • Ваши друзья не друзья вашего ребенка.
  • Друзья вашего ребенка - не ваши друзья.
  • Только друзья могут прикасаться к вашим интимным частям.

Для выполнения TDD я много раз использовал ключевое слово friend в C++.

Может ли друг знать обо мне все?


Обновлено: я нашел этот ценный ответ о ключевом слове "друг" с сайта Бьярна Страуструпа .

«Друг» - это явный механизм предоставления доступа, как и членство.

Один конкретный экземпляр, который я использую, friend - это создание классов Singleton . friend Ключевое слово позволяет мне создать функцию доступа, которая является более кратким , чем всегда иметь «GetInstance ()» метод на классе.

/////////////////////////
// Header file
class MySingleton
{
private:
    // Private c-tor for Singleton pattern
    MySingleton() {}

    friend MySingleton& GetMySingleton();
}

// Accessor function - less verbose than having a "GetInstance()"
//   static function on the class
MySingleton& GetMySingleton();


/////////////////////////
// Implementation file
MySingleton& GetMySingleton()
{
    static MySingleton theInstance;
    return theInstance;
}

Что касается оператора << и оператора >>, то нет никаких оснований заводить этих операторов в друзья. Это правда, что они не должны быть функциями-членами, но им также не нужно дружить.

Лучше всего создать общедоступные функции печати (ostream &) и чтения (istream &). Затем напишите оператор << и оператор >> в терминах этих функций. Это дает дополнительное преимущество, позволяя сделать эти функции виртуальными, что обеспечивает виртуальную сериализацию.

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

Достаточно сказать, что я бы не стал использовать ключевое слово friend как важный компонент вашего дизайна.

Еще одна распространенная версия примера Эндрю - ужасная кодовая пара.

parent.addChild(child);
child.setParent(parent);

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

class Parent;

class Object {
private:
    void setParent(Parent&);

    friend void addChild(Parent& parent, Object& child);
};

class Parent : public Object {
private:
     void addChild(Object& child);

     friend void addChild(Parent& parent, Object& child);
};

void addChild(Parent& parent, Object& child) {
    if( &parent == &child ){ 
        wetPants(); 
    }
    parent.addChild(child);
    child.setParent(parent);
}

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

Короткий ответ: используйте друга, если он действительно улучшает инкапсуляцию. Улучшение читабельности и удобства использования (операторы << и >> являются каноническим примером) также является хорошей причиной.

Что касается примеров улучшения инкапсуляции, хорошими кандидатами являются классы, специально разработанные для работы с внутренними компонентами других классов (на ум приходят тестовые классы).

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

Однако я не использую ключевое слово непосредственно в объявлениях классов, вместо этого я использую изящный шаблонный прием, чтобы добиться этого:

template<typename T>
class FriendIdentity {
public:
  typedef T me;
};

/**
 * A class to get access to protected stuff in unittests. Don't use
 * directly, use friendMe() instead.
 */
template<class ToFriend, typename ParentClass>
class Friender: public ParentClass
{
public:
  Friender() {}
  virtual ~Friender() {}
private:
// MSVC != GCC
#ifdef _MSC_VER
  friend ToFriend;
#else
  friend class FriendIdentity<ToFriend>::me;
#endif
};

/**
 * Gives access to protected variables/functions in unittests.
 * Usage: <code>friendMe(this, someprotectedobject).someProtectedMethod();</code>
 */
template<typename Tester, typename ParentClass>
Friender<Tester, ParentClass> & 
friendMe(Tester * me, ParentClass & instance)
{
    return (Friender<Tester, ParentClass> &)(instance);
}

Это позволяет мне делать следующее:

friendMe(this, someClassInstance).someProtectedFunction();

Работает по крайней мере на GCC и MSVC.

Другое использование: друга (+ виртуальное наследование) можно использовать, чтобы избежать наследования от класса (иначе: «сделать класс неуправляемым») => 1 , 2

Из 2 :

 class Fred;

 class FredBase {
 private:
   friend class Fred;
   FredBase() { }
 };

 class Fred : private virtual FredBase {
 public:
   ...
 }; 

edit: Чтение faq немного дольше Мне нравится идея перегрузки оператора << >> и добавления в качестве друга этих классов, однако я не уверен, как это не нарушает инкапсуляцию

Как бы это нарушило инкапсуляцию?

Вы нарушаете инкапсуляцию, когда разрешаете неограниченный доступ к элементу данных. Рассмотрим следующие классы:

class c1 {
public:
  int x;
};

class c2 {
public:
  int foo();
private:
  int x;
};

class c3 {
  friend int foo();
private:
  int x;
};

c1 это , очевидно , не инкапсулируется. Любой желающий может читать и изменять x в нем. У нас нет возможности обеспечить какой-либо контроль доступа.

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

c3 ? Это менее инкапсулировано? Разрешает ли неограниченный доступ к x ? Разрешает ли доступ неизвестным функциям?

Нет. Это позволяет только одной функции получить доступ к закрытым членам класса. Прямо как и c2 сделал. И точно так же c2, единственная функция, которая имеет доступ, - это не «какая-то случайная неизвестная функция», а «функция, указанная в определении класса». Точно так c2 же, просто взглянув на определения классов, мы можем увидеть полный список тех, у кого есть доступ.

Так как же это меньше инкапсулировано? Такое же количество кода имеет доступ к закрытым членам класса. И все, у кого есть доступ, указаны в определении класса.

friend не нарушает инкапсуляцию. Это заставляет некоторых Java-программистов чувствовать себя некомфортно, потому что, когда они говорят «ООП», они на самом деле имеют в виду «Java». Когда они говорят «Инкапсуляция», они не имеют в виду «частные члены должны быть защищены от произвольного доступа», но «класс Java, в котором единственными функциями, имеющими доступ к частным членам, являются члены класса», хотя это полная чушь для несколько причин .

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

Во- вторых, это не является ограничительным достаточно . Рассмотрим четвертый класс:

class c4 {
public:
  int getx();
  void setx(int x);
private:
  int x;
};

Это, согласно вышеупомянутому менталитету Java, идеально инкапсулировано. И все же он позволяет абсолютно любому читать и изменять x . Как это вообще имеет смысл? (подсказка: это не так)

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

Создатель C++ говорит, что не нарушает принципа инкапсуляции, и я процитирую его:

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

Более чем понятно ...