为什么有些运算符只能重载为成员函数,其他运算符可以重载为友元函数,而其余的则可以两者兼而有之?

发布于 2024-07-27 12:33:35 字数 118 浏览 7 评论 0原文

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

这些背后的理由是什么?

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

Why can some operators only be overloaded as member functions, other as non-member "free" functions and the rest of them as both?

What is the rationale behind those?

How to remember which operators can be overloaded as what (member, free, or both)?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

∞梦里开花 2024-08-03 12:33:35

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

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

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

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

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

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

    1. 用户定义的文字运算符"" name()

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

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

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

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

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

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

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

当然,C++ 标准并没有给出其基本原理 为什么事情会以这种方式完成(而且也没有早期做出这些决定的记录),我记得 Bjarne Stroustrup 的《C++ 的设计和演变》中可能会找到最好的理由。那里讨论了操作员,但似乎没有可用的电子版本,

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

The question lists three classes of operators. Putting them together on a list helps, I think, with understanding why a few operators are restricted in where they can be overloaded:

  1. Operators which have to be overloaded as members. These are fairly few:

    1. The assignment operator=(). Allowing non-member assignments seems to open the door for operators hijacking assignments, e.g., by overloading for different versions of const qualifications. Given that assignment operators are rather fundamental that seems to be undesirable.
    2. The function call operator()(). The function call and overloading rules are sufficiently complicated as is. It seems ill-advised to complicate the rules further by allowing non-member function call operators.
    3. The subscript operator[](). Using interesting index types it seems that could interfere with accesses to operators. Although there is little danger of hijacking overloads, there doesn't seem to be much gain but interesting potential to write highly non-obvious code.
    4. The class member access operator->(). Off-hand I can't see any bad abuse of overloading this operator a non-member. On the other hand, I also can't see any. Also, the class member access operator has rather special rules and playing with potential overloads interfering with these seems an unnecessary complication.

    Although it is conceivable to overload each of these members are a non-member (especially the subscript operator which is works on arrays/pointers and these can be on either side of the call) it seems surprising if, e.g., an assignment could be hijacked by a non-member overload which which is a better match than one of the member assignments. These operators are also rather asymmetric: you generally wouldn't want to support conversion on both sides of an expression involving these operators.

    That said, e.g., for a lambda expression library it would be nice if it were possible to overload all of these operators and I don't think there is an inherent technical reason to preventing these operators from being overloadable.

  2. Operators which have to be overloaded as non-member functions.

    1. The user-defined literal operator"" name()

    This operator is somewhat of an odd-ball and, arguably not really really an operator. In any case, there is no object to call this member on for which members could be defined: the left argument of user-defined literals are always built-in types.

  3. Not mentioned in the question but there are also operator which can't be overloaded at all:

    1. The member selector .
    2. The pointer-to-member object access operator .*
    3. The scope operator ::
    4. The ternary operator ?:

    These four operators were considered to be too fundamental to be meddled with at all. Although there was a proposal to allow overloading operator.() at some point there isn't strong support doing so (the main use case would be smart references). Although there are certainly some contexts imaginable where it would be nice to overload these operators, too.

  4. Operators which can be overloaded either as members or as non-members. This is the bulk of the operators:

    1. The pre- and post-increment/-decrement operator++(), operator--(), operator++(int), operator--(int)
    2. The [unary] dereference operator*()
    3. The [unary] address-of operator&()
    4. The [unary] signs operator+(), operator-()
    5. The logical negation operator!() (or operator not())
    6. The bitwise inversion operator~() (or operator compl())
    7. The comparisons operator==(), operator!=(), operator<(), operator>(), operator<=(), and operator>()
    8. The [binary] arithmetic operator+(), operator-(), operator*(), operator/(), operator%()
    9. The [binary] bitwise operator&() (or operator bitand()), operator|() (or operator bit_or()), operator^() (or operator xor())
    10. The bitwise shift operator<<() and operator>>()
    11. The logic operator||() (or operator or()) and operator&&() (or operator and())
    12. The operation/assignment operator@=() (for @ being a suitable operator symbol()
    13. The sequence operator,() (for which overloading actually kills the sequence property!)
    14. The pointer pointer-to-member access operator->*()
    15. The memory management operator new(), operator new[](), operator new[](), and operator delete[]()
    16. operator co_await() related to c++20 coroutines

    The operators which can be overloaded either as members or as non-members are not as necessary for fundamental object maintenance as the other operators. That is not to say that they are not important. In fact, this list contains a few operators where it is rather questionable whether they should be overloadable (e. g., the address-of operator&() or the operators which normally cause sequencing, i. e., operator,(), operator||(), and operator&&().

Of course, the C++ standard doesn't give a rationale on why things are done the way they are done (and there are also no records of the early days when these decisions where made). The best rationale can probably be found in "Design and Evolution of C++" by Bjarne Stroustrup. I recall that the operators were discussed there but there doesn't seem to be an electronic version available.

Overall, I don't think there are really strong reasons for the restrictions other than potential complication which was mostly not considered worth the effort. I would, however, doubt that the restrictions are likely to be lifted as the interactions with existing software are bound to change the meaning of some program in unpredictable ways.

征棹 2024-08-03 12:33:35

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

例如,假设类 A

A a1;
..
a1 = 42;

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

a1.operator=(42);

对于 的 LHS 上的事物来说没有意义。 不是 A 的实例,因此该函数必须是会员。

The rationale is that it would not make sense for them to be non-members, as the thing on the left-hand side of the operator must be a class instance.

For example, assuming a class A

A a1;
..
a1 = 42;

The last statement is really a call like this:

a1.operator=(42);

It would not make sense for the thing on the LHS of the . not to be an instance of A, and so the function must be a member.

深陷 2024-08-03 12:33:35

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

Because you can't modify the semantics of primitive types. It wouldn't make sense to define how operator= works on an int, how to deference a pointer, or how an array access works.

无人问我粥可暖 2024-08-03 12:33:35

这是一个例子:
当您超载 << 对于 class 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 运算符<<(std::ostream& os) 签名作为成员函数,您实际上最终会得到一个成员函数 std::ostream 运算符<< <(this, std::ostream& os) 这不会做你想做的事。
因此,您需要一个不是成员函数且可以访问成员数据的运算符(如果您的类 T 有要流式传输的私有数据,则运算符<< 需要是类 T 的友元)。

Here is one example:
When you are overloading the << operator for a class T the signature will be:

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

where the implementation needs to be

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

For the << operator the first argument needs to be the ostream object and the second argument your class T object.

If you try to define operator<< as a member function you will not be allowed to define it as std::ostream operator<<(std::ostream& os, T& objT).
This is because binary operator member functions can only take one argument and the invoking object is implicitly passed in as the first argument using this.

If you use the std::ostream operator<<(std::ostream& os) signature as a member function you will actually end up with a member function std::ostream operator<<(this, std::ostream& os) which will not do what you want.
Therefore you need a operator that is not a member function and can access member data (if your class T has private data you want to stream, operator<< needs to be a friend of class T).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文