Я все еще новичок в C++, пытаясь узнать больше об этом языке. Недавно я прочитал о концепции ADL (поиск, зависящий от аргумента) и идиоме «Скрытые друзья» (https://www.modernescpp.com/index.php/argument-dependent-lookup-and-hidden-friends). Насколько я понимаю ADL, в случае неквалифицированного вызова функции C++ ищет функцию не только в текущем пространстве имен, но и в пространстве имен типа аргумента.
Я не понимаю, в чем смысл идиомы «скрытый друг» и что именно означает «скрытый друг» (то есть, что в нем скрыто). Я понимаю, что дружественные функции класса не являются функциями-членами, но могут обращаться к закрытым членам класса. Однако я не понимаю, зачем они нужны. В приведенном в чтении примере кода указывается на необходимость друзей в данных функциях именно для общих перегрузок с двумя параметрами пользовательского класса. То есть в
class MyDistance{
public:
explicit MyDistance(double i):m(i){}
MyDistance operator +(const MyDistance& a, const MyDistance& b){
return MyDistance(a.m + b.m);
}
friend MyDistance operator -(const MyDistance& a, const MyDistance& b){
return MyDistance(a.m - b.m);
}
friend std::ostream& operator<< (std::ostream &out, const MyDistance& myDist){
out << myDist.m << " m";
return out;
}
private:
double m;
};
Перегрузка оператора + для класса не является другом, является функцией-членом и технически принимает 3 параметра MyDistance здесь, я полагаю, поскольку это функция-член (this) и принимает 2 дополнительных параметра, что делает ее недействительной.
Однако вместо того, чтобы иметь скрытого друга, не могли бы мы просто написать код как
class MyDistance{
public:
...
MyDistance operator +(const MyDistance& other){
return MyDistance(m + other.m);
}
...
};
Есть ли недостатки в написании кода таким образом? Является ли это медленнее (во время компиляции) каким-то образом из-за порядка, в котором С++ выполняет поиск (возможно, просматривая функции, не являющиеся членами, прежде чем рассматривать функции-члены)? Кроме того, что именно «идиома скрытого друга» должна «скрывать»? Дело в том, что сама функция определена в классе, а не снаружи?
Для меня это новая концепция, но эта кажется лучшей статьей о скрытых друзьях, хотя даже здесь я думаю, что часть кода немного шаткая.
«Скрытая» часть скрытого друга заключается в том, что его можно найти только с помощью ADL. Это ограничивает его использование случаями, когда у вас действительно есть объект типа класса, но исключает использование типов, преобразуемых только в тип. Иногда это то, что вы хотите. (Это было обнаружено для оператора пути<<, где широкая строка может быть преобразована в узкую строку с помощью временного path объекта. Упс!).
@BoP Почему скрытого друга может найти только ADL, что это значит? Я вижу, как ADL может его найти, поскольку он просматривает пространство имен типа аргумента, которое включает функцию друга. Однако разве это не тот случай, когда мы заставляем оператор + также перегружать функцию-член? Есть ли другой способ вызова функции +, если это не скрытый друг?
«Найден только ADL» означает, что «скрытый друг» виден только тогда, когда у нас уже есть объект типа класса. и поэтому загляните внутрь класса. В противном случае компилятор может сначала найти свободную функцию/оператор, и только потом рассматривать преобразования в тип класса для соответствия параметрам. Скрытый друг не виден за пределами класса, поэтому он никогда не будет рассматриваться на первом этапе.





Есть ли недостаток? Да, в приведенном выше примере С++ применяет разные правила к двум аргументам оператора +. В частности, левый аргумент должен быть объектом типа MyDistance, но правый аргумент может быть любым типом, преобразуемым в MyDistance.
Немного расширив свой пример
class MyDistance{
public:
...
MyDistance(int dist) { ... }
MyDistance operator+(const MyDistance& other) const {
return MyDistance(m + other.m);
}
...
};
С этим кодом
MyDistance x(1);
MyDistance y = x + 2;
законно, потому что есть преобразование из int в MyDistance, но это незаконно
MyDistance x(1);
MyDistance y = 2 + x;
потому что, учитывая объявление над левой частью +, должно быть MyDistance объектом.
Такой проблемы нет, когда operator+ является другом, в этом случае любой аргумент может быть преобразован в MyDistance, и обе версии приведенного выше кода допустимы.
Мы ожидаем, что operator+ будет симметричным, поэтому дружественная версия лучше, потому что она применяет одни и те же правила к обоим аргументам.
Во всяком случае, для меня это выглядит скорее как афера, чем за. Наличие таких неявных преобразований — плохая идея, и оператор, добавляющий дополнительное измерение свободы, может привести к проблематичным и неожиданным решениям.
@ ALX23z Это зависит от того, но если вы не хотите неявных преобразований, то в первую очередь не кодируйте их в своем классе. Вы можете получить неявные преобразования с любым методом написания бинарного оператора, единственная разница в том, применяются ли они симметрично или нет.
если бы я хотел, чтобы оператор принимал набор классов, я бы написал шаблон, который правильно объявляет, что он принимает, а не полагался на что-то столь непостоянное и ненадежное, как неявное преобразование.
так что да. Нет неявного преобразования. Добавляет ли оператору в друзья что-нибудь ценное?
Я понимаю что ты имеешь ввиду. Вы случайно не знаете, как компилятор C++ переходит от a+b к a.operator+(b) и к operator+(a, b)? Существуют ли другие возможные функции, которые C++ вызывает для разрешения a+b, и в каком порядке C++ пытается использовать эти функции? Я определил как оператор-член + перегрузка, так и скрытый друг, и похоже, что С++ (по крайней мере, в gcc) отдает приоритет оператору-члену. Приводит ли это к ускорению времени компиляции?
@JohnJames Вам придется спросить кого-то более опытного, чем я. Однако я не могу представить, чтобы эта проблема повлияла на скорость компиляции вашего кода.
Дэн Сакс выступил с отличным докладом о скрытых друзьях на CppCon2018. Он называется Новые друзья.
В дополнение к проблемам, описанным @john, шаблоны являются еще одной серьезной причиной для освоения идиомы «скрытых друзей».
Операторы вставки и извлечения потока operator<< и operator>> лучше всего писать в терминах std::basic_ostream и std::basic_istream, шаблонов, на которых основаны std::ostream и std::istream. Написанные таким образом, операторы будут работать с любым типом символов.
Когда объекты, которые вы читаете и записываете, сами по себе являются шаблонами, все может быстро усложниться. Если функции оператора вставки и извлечения потока не скрыты внутри класса объекта, а вместо этого написаны вне его, вы должны использовать параметры шаблона как для объекта, так и для потока. Когда операторные функции написаны как скрытые друзья, внутри класса объекта вам все равно нужно указать параметры шаблона, но только для потока (а не для объекта).
Предположим, например, что вы решили добавить параметр шаблона в класс MyDistance. Если operator<< не является скрытым другом, код может выглядеть следующим образом. Этот operator<< находится вне класса MyDistance и может быть найден без ADL.
Это полная программа (она работает):
#include <iostream>
#include <type_traits>
template< typename NumType >
class MyDistance {
static_assert(std::is_arithmetic_v<NumType>, "");
public:
explicit MyDistance(NumType i) :m(i) {}
// ...
// This is a declaration that says, in essence, "In the
// scope outside this class, there is visible a definition
// for the templated operator<< declared here, and that
// operator function template is my friend."
//
// Although it is a friend, it is not hidden.
//
// operator<< requires three template parameters.
// Parameter NumType2 is distinct from NumType.
template< typename charT, typename traits, typename NumType2 >
friend auto operator<<
(std::basic_ostream<charT, traits>& out,
const MyDistance<NumType2>& myDist)
->std::basic_ostream<charT, traits>&;
private:
NumType m;
};
// operator<< is not hidden, because it is defined outside
// of class MyDistance, and it is therefore visible in the
// scope outside class MyDistance. It can be found without ADL.
//
// Here we can use NumType, NumType2, T, or anything else
// as the third template parameter. It's just a name.
template< typename charT, typename traits, typename NumType >
auto operator<<
(std::basic_ostream<charT, traits>& out,
const MyDistance<NumType>& myDist)
-> std::basic_ostream<charT, traits>&
{
out << myDist.m << " m";
return out;
}
int main()
{
MyDistance<int> md_int{ 42 };
MyDistance<double> md_double{ 3.14 };
std::cout
<< "MyDistance<int> : " << md_int << '\n'
<< "MyDistance<double> : " << md_double << '\n';
return 0;
}
Когда код написан как скрытый друг, он становится чище и лаконичнее. Этот operator<< не виден в области вне класса MyDistance, и его можно найти только с помощью ADL.
Это также полная программа:
#include <iostream>
#include <type_traits>
template< typename NumType >
class MyDistance {
static_assert(std::is_arithmetic_v<NumType>, "");
public:
explicit MyDistance(NumType i) :m(i) {}
// ...
// operator<< has only the two template parameters
// required by std::basic_ostream. It is only visible
// within class MyDistance, so it is "hidden."
//
// You cannot scope to it either, using the scope resolution
// operator(::), because it is not a member of the class!
//
// It is truly hidden, and can only be found with ADL.
template< typename charT, typename traits>
friend auto operator<<
(std::basic_ostream<charT, traits>& out,
const MyDistance& myDist)
->std::basic_ostream<charT, traits>&
{
out << myDist.m << " m";
return out;
}
private:
NumType m;
};
int main()
{
MyDistance<int> md_int{ 42 };
MyDistance<double> md_double{ 3.14 };
std::cout
<< "MyDistance<int> : " << md_int << '\n'
<< "MyDistance<double> : " << md_double << '\n';
return 0;
}
Теперь представьте, что MyDistance — это более сложный объект с множеством шаблонных параметров, некоторые из которых сами могут быть шаблонными.
Несколько лет назад я создал класс RomanNumeral<IntType> для выполнения арифметических операций с римскими цифрами. Я также написал класс Rational<IntType> для выполнения арифметических действий с рациональными числами, где числитель и знаменатель хранились отдельно. Затем мне пришла в голову блестящая идея разрешить построение рациональных чисел с помощью римских цифр! Но я также хотел, чтобы класс Rational продолжал работать с целыми числами. Какой беспорядок! Потребовалась настоящая забота о том, чтобы операторы потока работали так, чтобы они выводили такие вещи, как: xiii/c.
Это отличное упражнение. Одна из вещей, которую вы узнаете, если попробуете, заключается в том, что скрытые друзья — ваши друзья!
Для операторов-членов нет реальных недостатков. Это просто синтаксис. В некоторых случаях может быть лучше написать свободную функцию, но в данном случае это не имеет значения.
friendтребуется для оператора, первый параметр которого не является экземпляром этого класса. Как оператор<<у вас здесь.