为什么非成员函数不能用于重载赋值运算符?

33

赋值运算符可以使用成员函数进行重载,但不能使用非成员friend函数:

class Test
{
    int a;
public:
    Test(int x)
        :a(x)
    {}
    friend Test& operator=(Test &obj1, Test &obj2);
};

Test& operator=(Test &obj1, Test &obj2)//Not implemented fully. just for test.
{
    return obj1;
}

这导致了以下错误:

错误 C2801:'operator='必须是非静态成员

为什么不能使用friend函数来重载赋值运算符?编译器允许使用friend来重载其他运算符,如+=-=。支持operator=的内在问题/限制是什么?

9个回答

39
首先,需要注意的是,这与操作符特别实现为"friend"没有任何关系。实际上,这是关于将复制赋值实现为成员函数或非成员(独立)函数的问题。无论该独立函数是否成为友元都完全无关紧要:它可能是,也可能不是,这取决于它想要访问类内部的内容。
现在,这个问题的答案在D&E书中给出(C++的设计和演化)。原因是编译器总是为类声明/定义一个成员复制赋值运算符(如果您没有声明自己的成员复制赋值运算符)。
如果语言还允许将复制赋值运算符声明为独立(非成员)函数,则可能会出现以下情况
// Class definition
class SomeClass {
  // No copy-assignment operator declared here
  // so the compiler declares its own implicitly
  ...
};

SomeClass a, b;

void foo() {
  a = b;
  // The code here will use the compiler-declared copy-assignment for `SomeClass`
  // because it doesn't know anything about any other copy-assignment operators
}

// Your standalone assignment operator
SomeClass& operator =(SomeClass& lhs, const SomeClass& rhs);

void bar() {
  a = b;
  // The code here will use your standalone copy-assigment for `SomeClass`
  // and not the compiler-declared one 
}

如上例所示,复制分配的语义将在翻译单元的中间发生变化 - 在声明独立运算符之前,编译器的版本将被使用。在声明后,将使用您的版本。程序的行为将根据您放置独立复制分配运算符的位置而改变。
这被认为是不可接受的危险(而且确实如此),因此C ++不允许将复制分配运算符声明为独立函数。
的确,在您特定的例子中,它恰好使用了一个友元函数,该运算符在类定义内部非常早地被声明(因为这就是朋友的声明方式)。因此,在您的情况下,编译器当然会立即知道您的运算符的存在。但是,从C ++语言的角度来看,一般问题与友元函数无关。从C ++语言的角度来看,它涉及成员函数与非成员函数以及非成员重载复制分配的完全禁止,原因如上所述。

1
@Chubsdad:是的,但那样做有什么意义呢?它总是会存在歧义。你将无法使用独立运算符。 - AnT stands with Russia
@curiousguy:我认为这是非常危险的,原因显而易见:一个看似无害的代码片段(用户定义的operator =的定义)的移动可能会完全改变周围代码的语义。如果某个代码片段在定义之前,现在又恰好在定义之后,那么它的语义将会发生变化,因为它将自动从编译器提供的operator =切换到用户定义的operator = - AnT stands with Russia
@AndreyT 这可能适用于所有具有可转换类型的重载情况。 - curiousguy
@curiousguy:当然。但显然,人们决定赋值运算符非常重要,因此必须给予特殊处理。同样,这是D&E书中提出的理由。 - AnT stands with Russia
你能删除隐式声明的赋值运算符吗? - Zebrafish
显示剩余5条评论

32

因为编译器提供的默认 operator=(逐成员复制)会始终优先。也就是说,你的友元operator=将永远不会被调用。

编辑:这个答案回答了问题中的

支持 = 操作符的固有问题/限制是什么?

部分。其他答案引用了标准中指出无法这样做的部分,但这很可能是为什么标准会以这种方式编写的原因。


6
抱歉,但这是完全错误的,并且没有任何意义。为什么编译器的运算符会优先考虑?对于那些可以声明为独立函数的运算符,声明成员版本和独立版本都会导致歧义,而不是成员函数“优先考虑”。那么,这个答案中的陈述背后的逻辑是什么? - AnT stands with Russia
4
@AndreyT和@Billy Oneal:你们两个都是正确的,只是在不同的情况下。如果赋值操作是在类方法内执行的,由于查找规则,成员函数(在这种情况下由编译器生成)将优先并隐藏命名空间作用域中的operator=。如果赋值发生在类范围之外,则会存在歧义,编译器将无法通过。虽然这不能用operator=进行测试,但可以使用operator+=(或任何其他既可实现为成员函数又可实现为自由函数的运算符)生成一个相当简单的测试。 - David Rodríguez - dribeas
如果赋值操作是在类方法内执行的话,由于查找规则,成员函数(在这种情况下由编译器生成)会优先执行。不,它不会。 - curiousguy
@curiousguy:我不明白为什么你会因为David的评论而对我的回答进行负评。(尽管据我所知,David是正确的) - Billy ONeal
@BillyONeal "你会因为David的评论而对我的回答进行负评",我显然没有这样做。 - curiousguy
显示剩余4条评论

8
因为有一些操作符必须是成员函数,这些操作符包括:
operator[]
operator=
operator()
operator->

还有类型转换操作符,例如operator int

尽管可能可以解释为什么operator=必须是成员函数,但他们的论点不能适用于列表中的其他操作符,这使我相信对于“为什么”的答案是“就是因为这样”。

希望这有所帮助。


我相信这是提问者的问题:为什么他们必须成为成员? - AnT stands with Russia
And运算符根本无法重载,有时这可能很遗憾,因为您无法实现smart_reference。 - CashCow
@CashCow:Stroustrup在《C++的设计与演化》一书中深入阐述了为什么不允许重载"."的原因。还有一些不能重载的运算符,比如::.* ?: sizeof等。 - Armen Tsirunyan
@CashCow “_And运算符无法重载_”,operator.()必须返回引用类型。引用什么? - curiousguy

8

$13.5.3 - "一个赋值运算符应该由一个只有一个参数的非静态成员函数实现。因为如果用户没有声明类的复制赋值运算符operator=,则会隐式声明一个(12.8),基类的赋值运算符总是被派生类的复制赋值运算符所隐藏。"


这并不能解释为什么例如运算符[]不能作为独立的函数进行重载,所以我想“这就是它的本来面目”是最准确的答案。 - Armen Tsirunyan
+1 -- 我回答的要点,但更好的标准引用支持它。 - Billy ONeal
@Armen Tsirunyan:是的。我试图引用标准中谈论这个方面的实际引用。Billy和其他答案给出了相同背后的好理由。 - Chubsdad
1
这个答案,特别是强调的部分,谈到了派生类和基类之间的名称隐藏。这与问题有什么关系? - AnT stands with Russia

3

operator=是一种特殊的成员函数,如果您不声明它,编译器将提供它。由于operator=的这种特殊状态,要求它是一个成员函数是有意义的,这样就不可能同时存在编译器生成的成员operator=和用户声明的友元operator=,也没有选择两者之间的可能性。


1
为什么友元函数不能用于重载赋值运算符?
简短回答:因为就是这样。
稍微长一点的回答:这是语法规定的方式。有一些运算符必须是成员函数,而赋值运算符就是其中之一。

0
因为类中已经有一个隐式的重载'='运算符的函数来进行浅拷贝,所以即使你使用友元函数进行重载,也永远无法调用它,因为我们所做的任何调用都会调用隐式的浅拷贝方法,而不是重载的友元函数。

0

operator= 的用意是对当前对象进行赋值操作。因此,LHS 或 lvalue 是相同类型的对象。

考虑 LHS 是整数或其他类型的情况。这是由 operator int() 或相应的 operator T() 函数处理的情况。因此,LHS 的类型已经定义好了,但非成员 operator= 函数可能会违反这个规定。

因此,应该避免使用它。


0

此帖适用于C++11

为什么有人想要一个非成员operator=呢?好吧,使用成员operator=,则以下代码是可能的:

Test const &ref = ( Test() = something ); 

这会创建一个悬空引用。使用非成员运算符可以解决这个问题:

Test& operator=(Test &obj1, Test obj2)

因为现在prvalue Test()将无法绑定到obj1。实际上,这个签名将强制我们永远不返回悬空引用(除非当然我们被提供了一个)——函数总是返回一个“有效”的lvalue,因为它强制要求使用lvalue调用。

然而,在C++11中,现在有一种方法可以指定只能在lvalue上调用成员函数,因此您可以通过编写成员函数来实现相同的目标:

Test &operator=(Test obj2) &
//                        ^^^

现在带有悬空引用的上述代码将无法编译。


注意:operator=应该通过值或常量引用获取右侧操作数。通过值获取在实现复制并交换惯用语时非常有用,这是一种编写安全(但不一定是最快)的复制赋值和移动赋值运算符的技术。

这是一段时间以前发布的内容,但我在尝试解决问题时遇到了这个,所以想提一下:建议使用按值传递的方式来实现operator=,然后通过交换来实现并不是一个好主意。默认的交换是通过移动构造和赋值来实现的,但是按值传递的operator=同时实现了复制和移动赋值。这意味着你仍然需要自己实现交换(以避免递归),因此你仍然需要实现相同数量的方法,但是你的移动赋值比必要的慢得多。 - Nir Friedman
更好的建议是:自己实现移动赋值运算符。这样,在大多数情况下,你就可以免费获得高效的交换(当然,你仍需实现复制/移动构造函数)。然后,如果你更注重强异常安全性而非性能,则使用 CAS 实现副本赋值;否则,请从头开始实现它。无论哪种方式,“operator=” 都不应该通过值传递。 - Nir Friedman
@NirFriedman 更新了。这实际上取决于类的细节。在按值传递的方法中,您只需要在一个地方进行交换,因此可以将交换逻辑放在operator=函数体内。CAS使您的代码非常简单且异常安全,但正如您所说,通常不是最有效的方法。 - M.M
按值赋值(统一赋值)对于异常安全性也不是很好,因为您的移动赋值通常应该是无异常的。但是在这里,因为它与您的复制赋值是相同的函数(虽然使用CAS时具有强保证),您不能轻松地将其标记为无异常。 - Nir Friedman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接