C++ 为什么建议在运算符重载中将函数声明为“友元”

C++ 为什么建议在运算符重载中将函数声明为“友元”,c++,c++11,C++,C++11,我在很多地方都读过这篇文章,建议在重载操作时使用friend,但没有人清楚地解释为什么真的需要它?为什么我们不能将它们声明为普通成员函数?有什么缺点吗 谷歌对此进行了搜索,但没有得到任何明确的答案。使用friend意味着它是一个非成员friend函数 为了通过最小化依赖关系来改进封装,最好声明非成员非朋友函数。如果需要访问类的私有/受保护成员,请将其设置为好友。最后,将其作为成员函数 下面是一个算法,用于根据[C++编码标准:101规则、指南和最佳实践(Herb Sutter,Andrei Al

我在很多地方都读过这篇文章,建议在重载操作时使用friend,但没有人清楚地解释为什么真的需要它?为什么我们不能将它们声明为普通成员函数?有什么缺点吗

谷歌对此进行了搜索,但没有得到任何明确的答案。

使用friend意味着它是一个非成员friend函数

为了通过最小化依赖关系来改进封装,最好声明非成员非朋友函数。如果需要访问类的私有/受保护成员,请将其设置为好友。最后,将其作为成员函数

下面是一个算法,用于根据[C++编码标准:101规则、指南和最佳实践(Herb Sutter,Andrei Alexandrescu)]第44项确定函数是否应该是成员和/或朋友。喜欢编写非成员非朋友函数:

// If you have no choice then you have no choice; make it a member if it must be:

If the function is one of the operators =, ->, [], or (), which must be members:

    Make it a member.

// If it can be a nonmember nonfriend, or benefits from being a nonmember friend, do it:

Else if: a) the function needs a different type as its left-hand argument (as do operators >> or <<, for example); or b) it needs type conversions on its leftmost argument; or c) it can be implemented using the class's public interface alone:

    Make it a nonmember (and friend if needed in cases a) and b) ).

    If it needs to behave virtually:

        Add a virtual member function to provide the virtual behavior, and implement the nonmember in terms of that.

Else: Make it a member.

在某些情况下,例如上面提到的a和b,您无法通过成员函数实现它们,您必须将它们声明为非成员函数,如果需要访问类的私有/受保护成员,则必须使它们成为朋友。

您列出了仅用于重载运算符的两个选项,而实际上有三个选项:

全球功能,而不是朋友 成员函数 全局函数,类的朋友 您没有列出第一个,但它是推荐的。如果可以根据现有类公共接口定义运算符,请将其定义为类外部的全局函数。通过这种方式,您无需扩展类公共接口,从而最大限度地减少了可以访问类私有成员的函数数量


如果操作员需要访问类私有成员,该怎么办?然后有两个选项-成员函数或友元全局函数。从这两个成员中选择函数更可取,因为它更干净。但是,在某些情况下,不可能将重载运算符定义为成员函数。如果类的对象是双参数运算符的右参数,则全局函数是唯一的选择。

我假设我们正在比较类中定义的全局范围友元和非友元。 前者有时被首选的原因是这些功能

。。。需要访问要执行的操作的数据成员。这也可以通过放松封装,通过公共getter提供数据来实现,但这并不总是需要的

。。。只能通过ADL找到,以避免污染某些名称空间和重载候选集。只有在类中定义的友元函数才满足这一点


此外,一个小优点是,对于类模板,更容易定义一个全局函数,该函数在它们内部的专门化上进行操作,因为我们避免了将该函数作为模板。该函数也是隐式内联的。所有这些都缩短了代码,但并不是主要原因。

人们使用friend有几个原因:

有时,授予友谊实际上是合理的,因为公共API不应该公开一些需要比较的成员

懒惰的程序员可以方便地授予对所有私有和受保护的数据成员的访问权,确保您可以编写操作员实现,而无需稍后返回授予访问权或使用不太明显/直接的公共函数,这不是一个好的理由,只是一个懒惰的理由

您可以在类中定义运算符函数,其中任何模板参数、typedef、常量等都不需要像在周围的[namespace]范围中那样显式限定。对于那些新的C++ ++来说,这是相当简单的。 e、 g:


有时不能将运算符重载声明为成员函数,如IO运算符。这些函数的第一个参数必须是ostream或istream,它们是库类,您不能扩展它们,将此类函数声明为friend可以让它们访问类的私有变量。

如果您的实现遵循了正确的数据封装,那么您可能不会将数据变量公开给外部世界,并且所有数据成员都将声明为私有

但是,在大多数情况下,使用运算符重载,您将访问数据成员,请注意,在这里,您将访问类外的数据成员。因此,为了提供对类外数据成员的访问,建议将运算符重载函数声明为friend

但一元运算符可能不需要这样做,因为它将对调用它的特定类的数据成员进行操作


如果您需要了解任何示例,请告诉我。

在处理运算符时,非成员函数有很多优点

大多数运算符都是二进制的,有两个参数,并且有点对称,对于成员运算符,只有当*这是左侧参数时,它们才起作用。因此,您需要使用非成员运算符

friend模式既允许操作员完全访问该类,而且操作员通常非常亲密,这样做不会造成伤害,并且使其在ADL之外不可见。此外,如果它是一个模板类,那么它还有显著的优势

ADL之外的隐形功能让你可以做这样疯狂的事情:

struct bob {
  template<class Lhs, class Rhs>
  friend bob operator+( Lhs&&, Rhs&& ) { /* implementation */ }
}
X有一个模板运算符==,而Y有一个朋友运算符==。模板版本必须与两个参数模式匹配才能为X,否则将失败。因此,当我传入一个X和一个可转换为X的类型时,它无法编译,因为模式匹配不进行用户定义的转换

另一方面,Y的运算符==不是模板函数。因此,当调用b==y时,通过ADL在y上找到它,然后测试b是否可以转换为y,调用成功


具有模式匹配的模板操作符是脆弱的。在标准库中,您可以从以下几个方面看到这个问题:运算符以阻止转换工作的方式重载。如果将该运算符声明为朋友运算符而不是公共免费模板运算符,则可以避免此问题。

@PiotrS。真的是这样吗?这个问题特别是关于friend本身的。@Columbo为了解决这个排序问题,我们将运算符重载函数定义为friend,如果它需要访问私有成员。这不是解决了这个问题吗?@PiotrS。是的,但还有更多。
    template <typename T>
    bool operator==(const X<T>& lhs, const X<T>& rhs) { ... }
struct X { X(int); };
bool operator==(const X& lhs, const X& rhs);

x == 3;   // ok for member or non-member operator==
3 == x;   // only works for non-member operator== after implicit X(3) for lhs
struct bob {
  template<class Lhs, class Rhs>
  friend bob operator+( Lhs&&, Rhs&& ) { /* implementation */ }
}
template<class T>
struct X {};

template<class T>
bool operator==( X<T>, X<T> ) { return true; }

template<class T>
struct Y {
  friend bool operator==( Y, Y ) { return true; }
};

struct A {
  template<class T>
  operator X<T>() const { return {}; }
};

struct B {
  template<class T>
  operator Y<T>() const { return {}; }
};


int main() {
  A a;
  X<int> x;
  B b;
  Y<int> y;
  b == y; // <-- works!
  a == x; // <-- fails to compile!
}