提问人:John James 提问时间:7/23/2023 最后编辑:John James 更新时间:7/29/2023 访问量:251
C++ 中的隐藏好友概念
Hidden Friend Concept in C++
问:
我仍然是 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 个参数,因为它是一个成员函数 (this) 并接受 2 个额外的参数,使其无效。MyDistance
但是,我们不能把代码写成
class MyDistance{
public:
...
MyDistance operator +(const MyDistance& other){
return MyDistance(m + other.m);
}
...
};
像这样写代码有什么缺点吗?由于 C++ 执行查找的顺序(也许在查看成员函数之前查看非成员函数),它在某种程度上(在编译时)变慢了吗?另外,“隐藏的朋友成语”到底应该“隐藏”什么?是函数本身是在类中定义的,而不是在类之外定义的吗?
答:
有什么缺点吗?是的,在上面的示例中,C++ 对运算符 + 的两个参数应用了不同的规则。具体来说,左手参数必须是类型的对象,但右手参数可以是任何可转换为 的类型。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+
评论
隐藏的朋友就是你的朋友
丹·萨克斯(Dan Saks)在CppCon2018上发表了精彩的演讲,解释了隐藏的朋友。它的标题是结交新朋友。
除了@john解释的问题外,模板是掌握“隐藏朋友”成语的另一个重要原因。
流插入和提取运算符,最好用 和 编写,以及 所基于的模板。以这种方式编写,运算符将适用于任何字符类型。operator<<
operator>>
std::basic_ostream
std::basic_istream
std::ostream
std::istream
当您正在读取和写入的对象本身就是模板时,事情会很快变得复杂。如果流插入和提取运算符函数未隐藏在对象类内部,而是写入对象类外部,则必须对对象和流使用模板参数。当运算符函数被写成隐藏的好友时,在对象类中,你仍然需要提供模板参数,但只针对流(而不是对象)。
例如,假设您决定向类添加一个模板参数。如果不是隐藏的好友,则代码可能如下所示。它驻留在类 MyDistance 之外的作用域中,可以在没有 ADL 的情况下找到。MyDistance
operator<<
operator<<
这是一个完整的程序(它运行):
#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;
}
当编写为隐藏的朋友时,代码既简洁又简洁。这在类 MyDistance 之外的作用域中不可见,只能在 ADL 中找到。operator<<
这也是一个完整的程序:
#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 是一个更复杂的对象,具有许多模板参数,其中一些参数本身可能已模板化。
几年前,我用罗马数字构建了算术课程。我还写了类来做有理数的算术,其中分子和分母是分开存储的。然后我萌生了一个好主意,允许用罗马数字构造有理数!但我也希望类 Rational 继续处理整数。真是一团糟!让流运算符工作非常小心,这样他们就会输出类似:xiii/c 之类的内容。RomanNumeral<IntType>
Rational<IntType>
这是一个很好的练习。如果你尝试一下,你会学到的一件事是,隐藏的朋友就是你的朋友!
评论
friend
<<
path