46

为什么某些运算符只能作为成员函数重载,而其他运算符只能作为非成员“自由”函数重载,而其余的则两者都重载?

这些背后的理由是什么?

如何记住哪些运算符可以重载为什么(成员、自由或两者)?

4

4 回答 4

33

该问题列出了三类运算符。我认为,将它们放在一个列表中有助于理解为什么一些运算符被限制在可以重载的位置:

  1. 必须作为成员重载的运算符。这些是相当少的:

    1. 任务operator=()。允许非成员分配似乎为操作员劫持分配打开了大门,例如,通过重载不同版本的const资格。鉴于赋值运算符是相当基本的,这似乎是不可取的。
    2. 函数调用operator()()。函数调用和重载规则已经足够复杂了。通过允许非成员函数调用运算符来进一步复杂化规则似乎是不明智的。
    3. 下标operator[]()。使用有趣的索引类型似乎会干扰对运算符的访问。尽管劫持重载的危险很小,但似乎没有太多收获,但编写高度不明显的代码的潜力却很有趣。
    4. 类成员访问operator->()。副手我看不出有任何严重滥用这个运算符重载一个非成员。另一方面,我也看不到任何东西。此外,类成员访问运算符具有相当特殊的规则,并且使用干扰这些的潜在重载似乎是不必要的复杂化。

    尽管重载这些成员中的每一个都是非成员是可以想象的(尤其是在数组/指针上工作的下标运算符,并且它们可以在调用的任一侧),但如果,例如,一个赋值可能被劫持,这似乎令人惊讶通过非成员重载,这比成员分配之一更好的匹配。这些运算符也相当不对称:您通常不希望在涉及这些运算符的表达式的两边都支持转换。

    也就是说,例如,对于 lambda 表达式库,如果可以重载所有这些运算符,那就太好了,我认为阻止这些运算符重载没有内在的技术原因。

  2. 必须作为非成员函数重载的运算符。

    1. 用户定义的文字operator"" name()

    这个操作员有点奇怪,可以说不是真正的操作员。在任何情况下,都没有可以为其定义成员的对象来调用此成员:用户定义文字的左参数始终是内置类型。

  3. 问题中没有提到,但也有根本不能重载的运算符:

    1. 成员选择器.
    2. 指向成员对象的访问运算符.*
    3. 范围运算符::
    4. 三元运算符?:

    这四个运营商被认为太基础了,根本无法干预。尽管有人提议operator.()在某些时候允许重载,但没有强烈支持这样做(主要用例是智能引用)。尽管在某些情况下可以想象重载这些运算符也会很好。

  4. 可以作为成员或非成员重载的运算符。这是大多数操作员:

    1. 前后递增/递减operator++(), operator--(), operator++(int),operator--(int)
    2. [一元] 取消引用operator*()
    3. [一元] 地址operator&()
    4. [一元] 符号operator+(),operator-()
    5. 逻辑否定operator!()(或operator not()
    6. 位反转operator~()(或operator compl()
    7. 比较operator==(), operator!=(), operator<(), operator>(), operator<=(), 和operator>()
    8. [二进制] 算术operator+(), operator-(), operator*(), operator/(),operator%()
    9. [二进制] 按位operator&()(or operator bitand()), operator|()(or operator bit_or()), operator^()(or operator xor())
    10. 按位移位operator<<()operator>>()
    11. 逻辑operator||()(or operator or()) 和operator&&()(or operator and())
    12. 操作/赋值operator@=()@作为合适的操作符 symbol()
    13. 序列operator,()(重载实际上会杀死序列属性!)
    14. 指向成员的指针访问operator->*()
    15. 内存管理operator new(), operator new[](), operator new[](), 和operator delete[]()

    可以作为成员或非成员重载的运算符对于基本对象维护而言不像其他运算符那样必要。这并不是说它们不重要。事实上,这个列表包含一些运算符,它们是否应该是可重载的相当值得怀疑(例如,地址operator&()或通常导致排序的运算符,即operator,(),operator||()operator&&().

当然,C++ 标准并没有给出为什么事情会按照他们的方式完成的理由(也没有关于做出这些决定的早期记录)。最好的理由可以在 Bjarne Stroustrup 的“Design and Evolution of C++”中找到。我记得那里讨论过操作员,但似乎没有可用的电子版本。

总的来说,除了潜在的并发症之外,我认为这些限制并没有真正的充分理由,而这些并发症大多被认为不值得付出努力。然而,我怀疑这些限制可能会被取消,因为与现有软件的交互势必会以不可预测的方式改变某些程序的含义。

于 2013-12-16T22:04:27.027 回答
8

基本原理是,将它们作为非成员是没有意义的,因为运算符左侧的东西必须是类实例。

例如,假设 A 类

A a1;
..
a1 = 42;

最后一条语句实际上是这样的调用:

a1.operator=(42);

对于. 不是 A 的实例,因此该函数必须是成员。

于 2009-07-15T16:56:47.097 回答
6

因为你不能修改原始类型的语义。定义如何operator=在 上工作int、如何引用指针或数组访问如何工作是没有意义的。

于 2009-07-15T16:58:43.300 回答
1

这是一个示例:当您重载<< operatorfor aclass T时,签名将是:

std::ostream operator<<(std::ostream& os, T& objT )

需要实施的地方

{
//write objT to the os
return os;
}

对于<<运算符,第一个参数需要是 ostream 对象,第二个参数需要是类 T 对象。

如果您尝试将其定义operator<<为成员函数,则不允许将其定义为 std::ostream operator<<(std::ostream& os, T& objT). 这是因为二元运算符成员函数只能接受一个参数,并且调用对象作为第一个参数隐式传入,使用this.

如果您将std::ostream operator<<(std::ostream& os)签名用作成员函数,您实际上最终会得到一个std::ostream operator<<(this, std::ostream& os)不会执行您想要的操作的成员函数。因此,您需要一个不是成员函数并且可以访问成员数据的运算符(如果您的 T 类有要流式传输的私有数据,则operator<<需要成为 T 类的朋友)。

于 2013-12-15T12:37:43.710 回答