Скрытые друзья в плюсах
Как известно, ключевое слово friend в C++ используется для предоставления доступа к закрытым членам класса внешним функциям и классам. Помимо этого, friend наделена еще одной фишкой, о которой знают далеко не все. В этой статье речь пойдет о hidden friends. Желающих разобраться в сабже, прошу под кат.
Существует определенный набор рекомендаций для сокращения времени компиляции программ, написанных на С++, и идиома hidden friends является одной из них.
Рассмотрим класс точки с координатами x и y:
namespace drawing
{
class point
{
int x_{0};
int y_{0};
public:
point() = default;
point(int x, int y) : x_(x), y_(y) {}
// методы всякие, хорошие и разные
};
}
Опустим детали реализации остальных методов, так как они нам сейчас не очень интересны. Допустим, нам нужно выводить координаты этой точки в std: cout в некотором незамысловатом формате и для этих целей требуется ввести оператор <<, который выглядит как-то так:
namespace drawing
{
std::ostream& operator << (std::ostream& os, const point& pt)
{
os << pt.x_ << ':' << pt.y_;
return os;
};
}
Поскольку нам нужен доступ к закрытым членам класса point, мы добавим friend-объявление в этот класс (а иначе, код просто не скомпилируется).
Приведу сразу полный код:
#include
namespace drawing
{
class point
{
int x_{0};
int y_{0};
public:
point() = default;
point(int x, int y) : x_(x), y_(y) {}
friend std::ostream& operator << (std::ostream& os, const point& pt);
};
std::ostream& operator << (std::ostream& os, const point& pt)
{
os << pt.x_ << ':' << pt.y_;
return os;
}
}
int main()
{
drawing::point pt{24, 42};
// do something
std::cout << pt << std::endl;
}
Все отлично собирается, запускается и отрабатывает с ожидаемым результатом.
Обратите внимание на то, что оператор << определен в пространстве имен drawing. Но тем не менее, компилятор нашел его, хотя вызов находится в глобальном пространстве. То есть компилятор взял и заменил
std::cout << pt << std::endl;
на
drawing::operator<<(std::cout, pt) << std::endl;
Это называется поиском Кёнига или поиском, зависимым от аргументов (ADL — argument-dependent lookup), когда компилятор при поиске функции просматривает еще и пространства, в которых находятся объявления типов аргументов функции. Благодаря этой возможности нам не нужно писать кучу лишнего когда и вообще перегрузка операторов потеряла бы всякий смысл.
Теперь давайте перенесем определение оператора в класс:
namespace drawing
{
class point
{
int x_{0};
int y_{0};
public:
point() = default;
point(int x, int y) : x_(x), y_(y) {}
friend std::ostream& operator << (std::ostream& os, const point& pt)
{
os << pt.x_ << ':' << pt.y_;
return os;
}
};
}
А че так можно было что-ли? ©
Это тоже нормально компилируется. Компилятор найдет нужный оператор в объявлении типа аргумента, при условии, что этот оператор объявлен дружественным.
Такая форма определения дружественной функции называется hidden friend и у нее есть свои преимущества.
Просто для интереса попробуйте убрать здесь friend и компилятор справедливо выругается, что аргументов у вашего оператор многовато, а хотелось бы всего один.
Давайте еще раз, закрепим чем отличается hidden friend от not hidden friend:
class point
{
int x_{0};
int y_{0};
public:
point() = default;
point(int x, int y) : x_(x), y_(y) {}
friend bool operator==(const point& lhs, const point& rhs); // not hidden friend
friend std::ostream& operator << (std::ostream& os, const point& pt) // hidden friend
{
os << pt.x_ << ':' << pt.y_;
return os;
}
};
bool operator==(const point& lhs, const point& rhs)
{
return lhs.x_ == rhs.x_ && lhs.y_ == rhs.y_;
}
Свободная дружественная функция не является скрытым другом. Определенная внутри, т.е. inline friend функция является нашим пациентом — скрытым другом. Думаю, понятно, что классы скрытыми друзьями быть не могут и мы здесь рассматриваем только функции (и на всякий случай, операторы — это тоже функции).
Преимущества скрытых друзей
Когда компилятор ищет функцию по ее неквалифицированному (т.е. без ::) имени, он просматривает глобальное пространство имен и пространство имен, в которых объявлены типы аргументов.
Скрытые дружественные функции не участвуют в разрешении перегрузки до тех пор, пока типы классов, в которых они определены не присутствуют в аргументах функции.
Представьте, что у нас большой проект, в котором сотни-тысячи классов, и у каждого свои нескрытые операторы. Компилятор в муках будет проверять все, что найдет, да еще пытаться неявно выполнять преобразования типов.
В результате, во-первых, можно найти совершенно левый, но подходящий по мнению компилятора оператор. Во-вторых, тратится впустую достаточно много времени (по некоторым оценкам, в разы и даже десятки раз больше). Скрытые друзья оберегают от этих проблем (нежелательное неявное преобразование типов и скорость компиляции), так как участвуют в поиске только при ADL и от полного или квалифицированного поиска спрятаны.
Обратите внимание на то, что вызов должен использовать неквалифицированные имена, иначе ADL будет проигнорирован. Разумеется, если аргументы отсутствуют, то ADL тоже не используется.
Недостатки скрытых друзей
На самом деле разрешение перегрузки обычно не такая уж и большая проблема, если говорить о времени компиляции. К примеру, включение лишних заголовков из стандартной библиотеки отнимет времени у компилятора гораздо больше. И если для определения скрытого друга в хедере требуется включить какой-нибудь другой тяжелый заголовочный файл, то тут надо трижды подумать, стоит ли оно того.
Рекомендация:
Следует избегать написания свободных дружественных функций в пользу определения дружественные функций внутри класса. Но следует учитывать включение других заголовочных файлов, которые могут для этого понадобиться.
Дополнительное чтиво:
https://www.justsoftwaresolutions.co.uk/cplusplus/hidden-friends.html
https://jacquesheunis.com/post/hidden-friend-compilation/