Как известно, ключевое слово 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 <iostream>

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 тоже не используется.

Недостатки скрытых друзей

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

Upd: Как справедливо отметили в комментариях, функцию не вынести в cpp. Может быть это и не проблема, но ограничение такое есть.

Рекомендация:

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

Дополнительное чтиво:

Комментарии (11)


  1. WildLynxDev
    18.03.2022 19:19
    -1

    Порядка 15 лет кодинга на С++ под Линукс, и я полез гуглить что означает = default.

    Я лошара?


    1. cdriper
      18.03.2022 20:59
      +10

      C++ это стандартизированный язык, поэтому Linux или Windows вообще никакой роли не играют.

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


    1. Naf2000
      18.03.2022 21:36
      +9

      Смотря какого стандарта с++ вы придерживаетесь в своём кодинге. default- конструктор был введён, если не ошибаюсь в c++11.


    1. Explorus Автор
      19.03.2022 12:10
      +2

      Мой опыт показывает, что когда человек говорит, что знает C++, на деле оказывается, что даже половины языка он ничерта не знает, не говоря уж об STL. Смотрит на лямбду, к примеру, и спрашивает, это точно С++? Но это не сильно мешает ему писать вполне грамотный код, быстрый и безопасный.


    1. Racheengel
      19.03.2022 13:25
      +3

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

      Имхо надо стараться к упрощению и сокращению конструкций, а пока выходит наоборот и это не особо радует...


  1. Playa
    18.03.2022 22:27
    +3

    Есть ещё один минус: такую функцию не вынесешь в .cpp


  1. SShtole
    19.03.2022 12:05

    А нельзя было просто сделать, чтобы все необходимые сущности умели преобразовываться в примитивы (типа, `.ToString()`), а вместо потока использовать форматирование?

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


    1. Sulerad
      19.03.2022 15:17

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


      1. SShtole
        19.03.2022 15:28

        Лишняя связь (между потоком и точкой) это тоже нехорошо.


        1. Deosis
          21.03.2022 07:29

          Здесь компромисс между быстродействием и независимостью.

          Если у вас карта с миллионом точек, то создавать временную строку - так себе идея.

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


          1. SShtole
            21.03.2022 10:07

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