Дружественные функции, методы и классы в C++. Критика.

Введённые ещё во времена, когда общая ООП парадигма не была окончательно сформирована, имеющиеся только в C++ реализации ООП парадигмы, т.н. «дружественные функции» стоят особняком в концепции ООП, и часто вызывают недоумения по многим причинам.

Сложность понимания

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

«Дружественные функции это функции, что не являются членами класса, но имеют доступ к его закрытым членам.»

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

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

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

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

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

Принцип работы

Настоящий функционал дружественных функций куда более скуден и противоречив. Единственное, что можно сделать внутри дружественных функций, это через уже имеющийся объект класса обратиться к его private членам, т.е. переменным и функциям, которые в классе этого объекта имеют модификатор доступа private. И всё! Больше ничего дружественные функции не делают. И даже для такого ошибочного, с точки зрения парадигмы ООП по части инкапсуляции, функционала приходится ещё написать много всего дополнительного:

  • Надо заранее указать эти дружественные функции, или целый класс, где они определены (это уже будет дружественный класс), в самом классе, к private членам которого нужен доступ через объект в этих функциях.
  • Надо как-то передать такой функции сам объект класса, с которым и будет происходить работа в этой функции. Если это именно функция, а не метод (функция класса), то объект обычно передаётся в эту функцию в виде её параметра, как правило, первого, что явл. ссылкой на объект. Если же это дружественный метод, то в его классе уже может быть указатель на объект нужного класса, что определил дружественные ему методы и классы.

Применение

В C++ дружественные функции часто применяют в перегрузке операторов внешними функциями — ещё одной отличительной особенности C++, когда функции переопределения операторов выносят за пределы классов, для объектов которых они предназначены. Из-за того, что перегрузку операторов внешними функциями часто делают с дружественными функциями, среди программистов C++ широко распространено заблуждение, что перегрузка операторов внешними функциями всегда делается с помощью дружественных функций и без них невозможна. Это не так! Здесь дружественные функции нужны только для упрощения. Это вовсе не обязательно. Больше нигде в C++ так часто не применяют дружественные функции, да и тут они необязательны.

«Костыль» на уровне ЯП

И всё это только ради того, чтобы исправить неправильный дизайн/архитектуру программы по части инкапсуляции. Неправильный он, потому что, если такая ситуация в программе возникла, когда надо получить доступ к закрытым членам объекта, то значит изначально инкапсуляция была сделана неправильно. Т.е. попавший в такую ситуацию программист уже делает что-то неправильно — как минимум, неправильно инкапсулирует сущности в классе.

И как это обычно бывает в подобных ситуациях, здесь начинаются «костыли», ведь переделывать дизайн/архитектуру долго. Таким костылём и являются дружественные функции. В др. ООП ЯП, напр., Java и C#, где нет возможности сделать всё это лишнее нагромождение с использованием дружественных функций, решается такая ситуация гораздо проще — нужному члену класса просто меняется модификатор доступа с private на public и всё. Понятно, что это неправильно, открывать члены, что изначально разработчиком класса были задуманы как закрытые, но сама ситуация вынуждает прибегать к подобным «костылям». Если разработчик попал в подобную ситуацию, это говорит о том, что он неправильно сделал дизайн/архитектуру, неправильно инкапсулировал. По хорошему счёту, в такой ситуации надо пересматривать дизайн/архитектуру приложения, но т.к. это долго, в ход идут «костыли». И тут разрабы ЯП C++ как бы изначально идут на встречу и говорят: «Вот ты неправильно инкапсулировал, но ничего страшного. У нас уже есть готовый костыль для этого случая — дружественные функции. Пользуйся. Мы одобряем. Это нормально.»

И разработчики это используют. Ещё бы, если сам ЯП это допускает, значит ничего плохого в этом нет. Приходится засорять класс всеми этими определениями функций или целых классов — его друзей. Но всё это неправильно. Если уж и делать «костыли», то как можно быстрее и меньше. Что-то где-то быстро подправить, чтобы заработало. Поэтому проще нужному закрытому члену класса просто изменить модификатор доступа на public, и, разумеется, пометить, что в будущем этот «костыль» надо убрать, всё это надо передизайнить, чтобы всё было по-нормальному.

Разработчики ЯП не могут ошибаться

Печально то, что такой костыль, что предусмотрен на уровне ЯП, направляет разработчиков в неправильные стороны, когда они думают, что это нормально, и начинают планировать дизайн/архитектуру инкапсуляции программы, отталкиваясь от этого, т.е. изначально закладывая в дизайн/архитектуру использование этого костыля. Логика тут простая: «Если уж сами разработчики ЯП ввели такую вещь, значит это не может быть плохо. Для чего-то это значит надо». Хочется использовать весь доступный функционал ЯП, применять максимально широкий спектр доступных в ЯП инструментов, т.к. их количество напрямую отражает уровень профессионализма разработчика.

Заключение

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

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