【问题标题】:Why does a friend operator require const-ness on its operands while a member operator does not?为什么友元运算符要求其操作数具有常量性,而成员运算符则不需要?
【发布时间】:2020-10-20 23:13:44
【问题描述】:

这可能与 taking references to temporary objects 仅在某些情况下允许使用有关,但我不确定此规则如何应用。

在此示例中,编译器在行a + b + c 处停止,因为a + b 返回一个Int,必须对其进行引用才能评估(a+b) + c。根据我所读到的,这是一个深思熟虑的选择,因此临时值不会发生突变。修复此代码的方法是将签名更改为Int operator + (const Int & a, const int & b)

struct Int {
    int a;
    Int() { a = 0; }
    Int(int i) { a = i; }
    friend Int operator + (Int &, const Int &);
};

Int operator + (Int & a, const Int & b) {
    return Int(a.a + b.a);
}

int main() {
    Int a, b, c;
    a + b + c;
    return 0;
}

但是,如果我们将操作符从 friend 函数更改为结构成员,那么这段代码(在创建临时可变值时应该有同样的问题)实际上可以编译:

struct Int {
    int a;
    Int() { a = 0; }
    Int(int i) { a = i; }
    Int operator + (Int &);
};

Int Int::operator + (Int & v)
{
    return Int(a + v.a);
}

int main()
{
    Int a, b, c;
    a + b + c;
    return 0;
}

为什么会发生这种情况?这两种情况看起来基本相同,但在后者中,不仅 RHS 值不需要const-ness,LHS 值也不需要。

【问题讨论】:

    标签: c++ compiler-errors


    【解决方案1】:

    在第一种情况下,代码实际上等效于:

    operator+(operator+(a, b), c);
    

    这里,表达式operator+(a, b)是一个右值,它不能绑定到一个非常量左值引用

    在第二种情况下,我们有:

    a.operator+(b).operator+(c);
    

    不存在这样的问题,因为只有 lvalues bc 绑定到那个 non-const lvalue reference 参数。


    正如@BenVoigt 指出的那样,您可能想知道在临时上调用非常量成员函数。这是完全合法的。标准中有一条关于它的特殊规则,在@dfrib的答案中有详细描述。

    【讨论】:

    • 问题是指出(a.operator+(b)) 绑定到一个非常量隐藏*this 参数
    【解决方案2】:

    总结:

    尝试a+(b+c) 并观察您的会员版本失败。

    C++ 具有命名值(又名左值,因为它们是在 = 赋值的左侧有意义的值)和临时值(又名右值,在 = 赋值的右侧进行) )。 (本段包含简化)。

    lvalues 将绑定到非 const 引用,而 rvalues 将只绑定到 const 引用。所以Int& 不会绑定到临时的,而Int const&const Int&(同样的事情)会。

    您得到的错误是 a+b+c 按 C++ 中的操作顺序被解析为 (a+b)+ca+b 的结果是一个临时的(右值)。所以它不会绑定到Int&

    Int operator+(Int& rhs) 这样的方法将*this 作为+ 的左侧参数,rhs 是右侧参数。所以(a+b)+ca+b*thiscrhs。成员operator+ 是临时工,所以一切都很好。

    Int operator+(Int&, Int const&) 这样的朋友——您会注意到Int&左侧(a+b)+c中的左边是加+c时的临时。所以它失败,因为a=b 不会绑定到Int&

    最后,成员绑定到临时对象的方式存在一些怪癖。因为成员在右值/左值引用之前添加到 C++ 20 多年,所以它们与临时对象的绑定比您预期的要宽松得多(默认情况下)。

    更详细:

    您的operator+ 成员在右侧采用非常量引用。您的朋友在左侧采用非常量引用。

    C++ 中的a+b+c 被解析为(a+b)+c。在任何情况下,+ 的右侧都有一个命名值——一个左值——并且命名的可变值可以绑定到非常量引用。

    但是,+c 在左侧有一个临时值 - 一个右值 - 并且临时值不能绑定到非 const 左值引用。

    最后一个怪癖与这样的成员有关:

    Int Int::operator + (Int & v)
    {
      return Int(a + v.a);
    }
    

    可以在右值(临时)上调用,即使它是非常量的。对象的 l/r 值限定是在初始成员语法之后添加的。

    您可以通过多种方式声明“该方法适用于哪种对象,常量或非常量,临时或非临时对象”。他们的签名看起来像:

    Int operator + (Int const& v);
    Int operator + (Int const& v) const;
    Int operator + (Int const& v) &;
    Int operator + (Int const& v) const&;
    Int operator + (Int const& v) &&;
    Int operator + (Int const& v) const&&;
    

    如果您不包含尾随 &&&,您的方法将绑定到 右值(临时)和左值(具有标识的命名值)。这是因为 l/r 值引用是在方法添加到 C++ 后 20 多年才添加的,因此 default 方法声明适用于两者(出于向后兼容性的原因)。

    friend Int operator + (Int &, const Int &);
    

    对应

    Int operator + (const Int &) &;
    

    作为一种方法,它会失败并显示与朋友基本相同的错误消息。

    你也可以诱导

    friend Int operator + (Int &, Int const&);
    

    通过键入来工作的方法:

    a+(b+c)
    

    因为现在+c 的左侧是临时的,并且拒绝绑定到Int&

    这也会使

    Int operator+(Int&)
    

    版本失败,因为现在a+的右边是(b+c),这是一个临时的,不会绑定到Int&

    一般建议:

    在为类处理运算符时,请执行此操作。

    Int operator+=(Int const& rhs)&;
    

    这是 increment by 运算符。它应该是一个成员,并且最后应该有 & 限定符——增加一个临时值是没有意义的。

    然后写:

    friend Int operator+(Int lhs, Int const& rhs) {
      lhs += rhs;
      return lhs;
    }
    

    这里我们使用增量运算符来实现+。如果我们的类有高效的移动构造函数(应该如此),这也将非常接近最优。

    我们采用的左侧参数。然后我们增加它。然后我们返回它(这会隐式移动)。

    当你输入时:

    a+b+c
    

    这变成了

    (a+b)+c
    

    我们复制a 并得到:

    copy of a += b
    

    然后我们将 a 的副本移动到结果中。然后将其省略(零成本移动)到+c 的左侧参数中。

    然后我们将其递增,并将其移动到结果中。

    因此,消除移动(即使在最复杂的对象上也应该接近零成本),我们得到:

    Int temporary = a;
    temporary += b;
    temporary += c;
    return temporary;
    

    a+b+c 的结果。

    这种模式适用于简单的东西,比如假整数,更复杂的东西,比如字符串,以及无数其他情况。它为您提供+=+ 的编码工作量与通常编写+ 所做的工作量大致相同。

    【讨论】:

      【解决方案3】:

      右值不能绑定到非常量左值引用

      friend Int operator + (Int &, const Int &);
      

      在此签名中,左侧参数是非常量引用,并且右值不能绑定到非常量左值引用以延长其生命周期。

         a + b + c;
      // ^^^^^ - result: rvalue (temporary)
      // -> (rvalue) + c
      // -> rvalue cannot bind to non-const lvalue reference.
      

      但是,它们可能会绑定到 const 左值引用以延长它们的生命周期。

      这两种情况看起来基本相同,[...]

      成员函数方法的一个可表示示例是将成员函数 CV 和 ref 限定为非常量左值 ref:

      Int operator+(Int &) &;  // ref-qualifier: &
                               // const-qualifier: non (non-const)
      

      在这种情况下,您将收到与上述friend 函数类似的错误。


      同理,编译示例

      friend Int 运算符 + (Int &, const Int &);

      相当于const&-限定成员函数:

      Int operator+(Int &) const &;
      

      作为右值 *this 可以绑定到 const &,从而延长其生命周期。


      没有引用限定符的非静态成员函数例外,即使隐式对象参数可能是非 const 左值引用类型

      根据[over.match.funcs]/4,如果没有提供引用限定符,那么隐式对象参数的类型是左值引用,因此接受非引用限定的情况可能会让人感到意外:[强调我的]:

      对于非静态成员函数,隐式对象参数的类型为

      • “对 cv X 的左值引用”用于声明没有引用限定符或使用 & 引用限定符
      • 的函数

      但是,正如 [over.match.funcs]/5 所涵盖的,对于未提供 ref 限定符的情况,这是一个明确的例外 [emphasis mine]:

      [...] 对于非静态成员函数声明时不带 ref 限定符,适用附加规则

      • 即使隐式对象形参不是 const 限定的,可以将右值绑定到形参,只要在所有其他方面可以将实参转换为隐式对象形参的类型.

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-09-12
        • 2019-02-08
        相关资源
        最近更新 更多