【问题标题】:Why is it recommended to declare functions as "friends" in operator overloading [duplicate]为什么建议在运算符重载中将函数声明为“朋友”[重复]
【发布时间】:2015-01-29 06:44:47
【问题描述】:

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

用谷歌搜索,但没有得到任何明确的答案。

【问题讨论】:

  • @PiotrS。真的吗?这个问题与friend 本身有关。
  • @Columbo “为了解决这个排序问题,我们将运算符重载函数定义为友元,如果它需要访问私有成员。”,这不是解决了这个问题吗?
  • @PiotrS。是的,但还有更多。

标签: c++ c++11


【解决方案1】:

有时您不能将运算符重载声明为成员函数,例如在 IO operator<<operator>> 中。这些函数的第一个参数必须是ostreamistream,它们是库类,你不能扩展它们,声明像friend 这样的函数可以让它们访问你的类的私有变量。

【讨论】:

    【解决方案2】:

    使用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),你不能通过成员函数来实现,你必须声明它们为非成员函数,如果需要访问私有/,则将它们设为friend类的受保护成员。

    【讨论】:

      【解决方案3】:

      人们使用friend有几个原因:

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

      • 1234563直接公共函数(这不是一个很好的理由,只是一个懒惰的理由)
      • 您可以在类中定义运算符函数,其中任何模板参数、类型定义、常量等都不需要像在周围的 [namespace] 范围内那样显式限定。对于 C++ 新手来说,这要简​​单得多。

      例如:

          template <typename T>
          struct X
          {
              friend bool operator==(const X& lhs, const X& rhs) { ... }
          };
      

      ...对... ...结构 X 同上,没有 ==...

          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
      

      【讨论】:

        【解决方案4】:

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

        • 全局函数,不是朋友
        • 成员函数
        • 全局函数,类友

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

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

        【讨论】:

          【解决方案5】:

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

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

          • ... 应只能通过 ADL 找到,以避免污染某些命名空间和重载候选集。 (只有在类中定义的friend 函数才是满足这一点的friend!)

          此外,一个小好处是,对于类模板,更容易定义一个全局函数来操作它们内部的特化,因为我们避免使函数成为模板。该函数也隐式为inline。所有这些都缩短了代码,但不是主要原因。

          【讨论】:

            【解决方案6】:

            如果您的实现遵循适当的数据封装,那么您可能不会将数据变量暴露给外界,并且所有数据成员都将被声明为私有。

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

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

            如果您需要任何示例来帮助您理解,请告诉我。

            【讨论】:

              【解决方案7】:

              在处理运算符时,非成员函数有很大的优势。

              大多数运算符是二元的(带两个参数)并且有些对称,并且对于成员运算符,它们仅在 *this 是左侧参数时才有效。所以你需要使用非成员运算符。

              友元模式既赋予操作员对类的完全访问权限(而且,操作员通常足够亲密以至于这无害),而且它使操作员在 ADL 之外不可见。此外,如果它是template 类,则具有显着的优势。

              在 ADL 之外不可见让你做这样疯狂的事情:

              struct bob {
                template<class Lhs, class Rhs>
                friend bob operator+( Lhs&&, Rhs&& ) { /* implementation */ }
              }
              

              这里我们的operator+template,看起来可以匹配任何东西。除了因为它只能在bob 上通过ADL 找到之外,只有在至少一个bob 对象上使用它才会匹配。这种技术可以让您了解右值/左值重载,对类型的属性进行 SFINAE 测试等。

              template 类型的另一个优点是运算符最终不是template 函数。 look here:

              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!
              }
              

              X&lt;T&gt; 有一个template operator==,而Y&lt;T&gt; 有一个friend operator==template 版本必须将两个参数模式匹配为X&lt;T&gt;,否则会失败。因此,当我传入 X&lt;T&gt; 和可转换为 X&lt;T&gt; 的类型时,它无法编译,因为模式匹配不会进行用户定义的转换。

              另一方面,Y&lt;T&gt;operator== 不是template 函数。所以当b == y被调用时,找到了(通过y上的ADL),然后对b进行测试,看是否可以转换为y(可以),调用成功。

              template 具有模式匹配的运算符很脆弱。您可以在标准库中的几个点看到这个问题,其中运算符以阻止转换工作的方式重载。如果将运算符声明为friend operator 而不是公共免费的template 运算符,则可以避免此问题。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2010-12-26
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-05-01
                相关资源
                最近更新 更多