运算符重载的基本规则和习惯用法是什么?

发布于 2025-01-15 23:14:26 字数 1574 浏览 2 评论 0 原文

注意:答案是按特定顺序给出的,并且随着时间的推移收到了不同数量的投票。由于答案的顺序取决于您的答案排序偏好,因此以下是按最有意义的顺序排列的答案索引

(注意:这是 Stack Overflow 的 C++ 常见问题解答。如果您想批评以这种形式提供常见问题解答的想法,那么开始这一切的元上的帖子就是这个地方这样做。该问题的答案在 C++ 聊天室 中进行监控,常见问题解答的想法最初是从这里开始的,因此你的答案很可能会被提出这个想法的人阅读。)

Note: The answers were given in a specific order, and have received varying amounts of votes over time. Since the order of answers depends on your answer sorting preferences, here's an index of the answers in the order in which they make the most sense:

(Note: This is meant to be an entry to Stack Overflow's C++ FAQ. If you want to critique the idea of providing an FAQ in this form, then the posting on meta that started all this would be the place to do that. Answers to that question are monitored in the C++ chatroom, where the FAQ idea started in the first place, so your answer is very likely to get read by those who came up with the idea.)

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

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

发布评论

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

评论(10

深居我梦 2025-01-22 23:14:27

要重载的常见运算符

重载运算符的大部分工作都是样板代码。这不足为奇,因为运算符只是语法糖。他们的实际工作可以通过(并且通常被转发到)普通函数来完成。但重要的是您要正确获取此样板代码。如果失败,要么您的操作员的代码将无法编译,您的用户的代码将无法编译,或者您的用户的代码将表现出令人惊讶的行为。

赋值运算符

关于赋值有很多话要说。然而,大部分内容已经在 GMan 著名的复制和交换常见问题解答中说了,所以我在这里跳过大部分内容,只列出完美的赋值运算符以供参考:

X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}

流插入和提取

免责声明
对于重载 <<>> 作为按位移位运算符,请跳至二元算术运算符部分。

按位移位运算符 <<>> 虽然仍然在硬件接口中用于它们从 C 继承的位操作函数,但已变得更加普遍,因为大多数应用程序中重载的流输入和输出运算符。

流运算符是最常见的重载运算符之一,是二进制中缀运算符,其语法没有指定它们是否应该是成员或非成员的任何限制。
但是,它们的左操作数是来自标准库的流,您无法向它们添加成员函数1,因此您需要为自己的类型实现这些运算符作为非成员函数2< /sup>.
两者的规范形式如下:

std::ostream& operator<<(std::ostream& os, const T& obj)
{
  // Write obj to stream
  return os;
}

std::istream& operator>>(std::istream& is, T& obj)
{
  // Read obj from stream
  if( /* no valid object of T found in stream */ )
    is.setstate(std::ios::failbit);
  return is;
}

在实现operator>>时,仅当读取本身成功时才需要手动设置流的状态,但结果不是预期的。

1 请注意,标准库的一些 << 重载是作为成员函数实现的,还有一些是作为自由函数实现的。只有与语言环境相关的函数才是成员函数,例如operator<<(long)

2 根据经验规则,插入/提取运算符应该是成员函数,因为它们修改左操作数。但是,我们在这里不能遵循经验法则。

函数调用运算符

函数调用运算符,用于创建函数对象,也称为 函子,必须定义为成员函数,因此它始终具有隐式this 成员函数的参数。除此之外,它可以被重载以接受任意数量的附加参数,包括零。

下面是语法示例:

struct X {
    // Overloaded call operator
    int operator()(const std::string& y) {
        return /* ... */;
    }
};

用法:

X f;
int a = f("hello");

在整个 C++ 标准库中,函数对象总是被复制。因此,您自己的函数对象的复制成本应该很低。如果函数对象绝对需要使用复制成本高昂的数据,那么最好将该数据存储在其他地方并让函数对象引用它。

比较运算符

此部分已移至其他地方
请参阅此常见问题解答以了解重载二进制文件中缀 ==!=<><=>= 运算符,以及 <=> 三向比较,又名。 C++20 中的“宇宙飞船运算符”。关于比较运算符有很多话要说,这超出了这个答案的范围。

在最简单的情况下,您可以通过在 C++20

#include <compare>

struct X {
  // defines ==, !=, <, >, <=, >=, <=>
  friend auto operator<=>(const X&, const X&) = default;
};

如果您无法执行此操作,请继续查看链接的答案。

逻辑运算

符 一元前缀否定 ! 应作为成员函数实现。超载通常不是一个好主意,因为它非常罕见且令人惊讶。

struct X {
  X operator!() const { return /* ... */; }
};

其余的二元逻辑运算符(||&&)应作为自由函数实现。但是,您非常不太可能找到这些1的合理用例。

X operator&&(const X& lhs, const X& rhs) { return /* ... */; }
X operator||(const X& lhs, const X& rhs) { return /* ... */; }

1 需要注意的是,内置版本的||&&使用了快捷语义。而用户定义的(因为它们是方法调用的语法糖)不使用快捷语义。用户期望这些运算符具有快捷语义,并且它们的代码可能依赖于它,因此强烈建议永远不要定义它们。

算术运算

符 一元算术运算

符 一元递增和递减运算符有前缀和后缀两种风格。为了区分其中一个,后缀变体需要一个额外的虚拟 int 参数。如果重载增量或减量,请确保始终实现前缀和后缀版本。

这是增量的规范实现,减量遵循相同的规则:

struct X {
  X& operator++()
  {
    // Do actual increment
    return *this;
  }
  X operator++(int)
  {
    X tmp(*this);
    operator++();
    return tmp;
  }
};

请注意,后缀变体是根据前缀实现的。另请注意,postfix 会进行额外的复制。1

重载一元减号和加号并不常见,最好避免。如果需要,它们可能应该作为成员函数重载。

1 另请注意,后缀变体执行更多工作,因此使用效率低于前缀变体。这是通常更喜欢前缀增量而不是后缀增量的一个很好的理由。虽然编译器通常可以优化内置类型的后缀增量的额外工作,但它们可能无法对用户定义类型执行相同的操作(这可能看起来像列表迭代器一样无辜)。一旦你习惯了 i++,当 i 不是内置的时,就很难记住执行 ++i 了。类型(另外,更改类型时您必须更改代码),因此最好养成始终使用前缀增量的习惯,除非明确需要后缀。

二元算术运算符

对于二元算术运算符,请执行以下操作 :不要忘记遵守第三个基本规则运算符重载:如果您提供+,还要提供+=,如果提供-,不要省略-=Andrew Koenig 据说是第一个观察到复合赋值运算符可以用作非复合对应物的基础。即运算符+是用+=实现的,-是用-=实现的, 根据我们的

经验规则,+ 及其同伴应该是非成员,而它们的复合赋值对应项(+= 等)则更改其左侧参数,应该是会员。以下是 +=+ 的示例代码;其他二元算术运算符应以相同的方式实现:

struct X {
  X& operator+=(const X& rhs)
  {
    // actual addition of rhs to *this
    return *this;
  }
};

inline X operator+(const X& lhs, const X& rhs)
{
  X result = lhs;
  result += rhs;
  return result;
}

operator+= 返回每个引用的结果,而 operator+ 返回其结果的副本。当然,返回引用通常比返回副本更有效,但在运算符+的情况下,没有办法绕过复制。当您编写 a + b 时,您希望结果是一个新值,这就是 operator+ 必须返回新值的原因。1

另请注意,通过按值而不是引用传递 lhs 可以稍微缩短 operator+
但是,这会泄漏实现细节,使函数签名不对称,并且会阻止命名返回值优化,其中结果与返回的对象是同一对象。

有时,用 @= 来实现 @ 是不切实际的,例如矩阵乘法。
在这种情况下,您还可以将 @= 委托给 @

struct Matrix {
  // You can also define non-member functions inside the class, i.e. "hidden friends"
  friend Matrix operator*(const Matrix& lhs, const Matrix& rhs) {
    Matrix result;
    // Do matrix multiplication
    return result;
  }
  Matrix& operator*=(const Matrix& rhs)
  {
    return *this = *this * rhs; // Assuming operator= returns a reference
  }
};

位操作运算符 ~ & | ^ << >> 应以与算术运算符相同的方式实现。然而,(除了输出和输入的重载 <<>>),重载这些的合理用例很少。

1 再次,从中吸取的教训是,一般来说,a += ba + b 更高效> 如果可能的话应该优先考虑。

下标运算符 下

标运算符是一个二元运算符,必​​须作为类成员实现。它用于类似容器的类型,允许通过键访问其数据元素。
提供这些的规范形式是这样的:

struct X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) const;
  // ...
};

除非您不希望您的类的用户能够更改 operator[] 返回的数据元素(在这种情况下您可以省略非常量变体) ,您应该始终提供运算符的两种变体。

类似指针类型的运算符

要定义自己的迭代器或智能指针,必须重载一元前缀取消引用运算符 * 和二进制中缀指针成员访问运算符 ->

struct my_ptr {
        value_type& operator*();
  const value_type& operator*() const;
        value_type* operator->();
  const value_type* operator->() const;
};

请注意,这些也几乎总是需要 const 和非 const 版本。
对于 -> 运算符,如果 value_type 属于 class(或 structunion) 类型,另一个 operator->() 被递归调用,直到 operator->() 返回非类类型的值。

一元取址运算符永远不应该被重载。

对于 operator->*() (以及有关 operator-> 的更多详细信息),请参阅 这个问题operator->*() 很少使用,因此很少重载。事实上,即使迭代器也不会重载它。


继续转换运算符

Common Operators to Overload

Most of the work in overloading operators is boilerplate code. That is little wonder, since operators are merely syntactic sugar. Their actual work could be done by (and often is forwarded to) plain functions. But it is important that you get this boilerplate code right. If you fail, either your operator’s code won’t compile, your users’ code won’t compile, or your users’ code will behave surprisingly.

Assignment Operator

There's a lot to be said about assignment. However, most of it has already been said in GMan's famous Copy-And-Swap FAQ, so I'll skip most of it here, only listing the perfect assignment operator for reference:

X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}

Stream Insertion and Extraction

Disclaimer
For overloading << and >> as bitwise shift operators, skip to the section Binary Arithmetic Operators.

The bitwise shift operators << and >>, although still used in hardware interfacing for the bit-manipulation functions they inherit from C, have become more prevalent as overloaded stream input and output operators in most applications.

The stream operators, among the most commonly overloaded operators, are binary infix operators for which the syntax does not specify any restriction on whether they should be members or non-members.
However, their left operands are streams from the standard library, and you cannot add member functions to those1, so you need to implement these operators for your own types as non-member functions2.
The canonical forms of the two are these:

std::ostream& operator<<(std::ostream& os, const T& obj)
{
  // Write obj to stream
  return os;
}

std::istream& operator>>(std::istream& is, T& obj)
{
  // Read obj from stream
  if( /* no valid object of T found in stream */ )
    is.setstate(std::ios::failbit);
  return is;
}

When implementing operator>>, manually setting the stream’s state is only necessary when the reading itself succeeded, but the result is not what would be expected.

1 Note that some of the << overloads of the standard library are implemented as member functions, and some as free functions. Only the locale-dependent functions are member functions, such as operator<<(long).

2 According to the rules of thumb, the insertion/extraction operators should be member functions because they modify the left operand. However, we cannot follow the rules of thumb here.

Function Call Operator

The function call operator, used to create function objects, also known as functors, must be defined as a member function, so it always has the implicit this argument of member functions. Other than this, it can be overloaded to take any number of additional arguments, including zero.

Here's an example of the syntax:

struct X {
    // Overloaded call operator
    int operator()(const std::string& y) {
        return /* ... */;
    }
};

Usage:

X f;
int a = f("hello");

Throughout the C++ standard library, function objects are always copied. Your own function objects should therefore be cheap to copy. If a function object absolutely needs to use data which is expensive to copy, it is better to store that data elsewhere and have the function object refer to it.

Comparison Operators

This section has been moved elsewhere
See this FAQ answer for overloading the binary infix ==, !=, <, >, <=, and >= operators, as well as the <=> three-way comparison, aka. "spaceship operator" in C++20. There is so much to say about comparison operators that it would exceed the scope of this answer.

In the most simple case, you can overload all comparison comparison operators by defaulting <=> in C++20:

#include <compare>

struct X {
  // defines ==, !=, <, >, <=, >=, <=>
  friend auto operator<=>(const X&, const X&) = default;
};

If you can't do this, continue to the linked answer.

Logical Operators

The unary prefix negation ! should be implemented as a member function. It is usually not a good idea to overload it because of how rare and surprising it is.

struct X {
  X operator!() const { return /* ... */; }
};

The remaining binary logical operators (||, &&) should be implemented as free functions. However, it is very unlikely that you would find a reasonable use case for these1.

X operator&&(const X& lhs, const X& rhs) { return /* ... */; }
X operator||(const X& lhs, const X& rhs) { return /* ... */; }

1 It should be noted that the built-in version of || and && use shortcut semantics. While the user defined ones (because they are syntactic sugar for method calls) do not use shortcut semantics. User will expect these operators to have shortcut semantics, and their code may depend on it, Therefore it is highly advised NEVER to define them.

Arithmetic Operators

Unary Arithmetic Operators

The unary increment and decrement operators come in both prefix and postfix flavor. To tell one from the other, the postfix variants take an additional dummy int argument. If you overload increment or decrement, be sure to always implement both prefix and postfix versions.

Here is the canonical implementation of increment, decrement follows the same rules:

struct X {
  X& operator++()
  {
    // Do actual increment
    return *this;
  }
  X operator++(int)
  {
    X tmp(*this);
    operator++();
    return tmp;
  }
};

Note that the postfix variant is implemented in terms of prefix. Also note that postfix does an extra copy.1

Overloading unary minus and plus is not very common and probably best avoided. If needed, they should probably be overloaded as member functions.

1 Also note that the postfix variant does more work and is therefore less efficient to use than the prefix variant. This is a good reason to generally prefer prefix increment over postfix increment. While compilers can usually optimize away the additional work of postfix increment for built-in types, they might not be able to do the same for user-defined types (which could be something as innocently looking as a list iterator). Once you got used to do i++, it becomes very hard to remember to do ++i instead when i is not of a built-in type (plus you'd have to change code when changing a type), so it is better to make a habit of always using prefix increment, unless postfix is explicitly needed.

Binary Arithmetic Operators

For the binary arithmetic operators, do not forget to obey the third basic rule operator overloading: If you provide +, also provide +=, if you provide -, do not omit -=, etc. Andrew Koenig is said to have been the first to observe that the compound assignment operators can be used as a base for their non-compound counterparts. That is, operator + is implemented in terms of +=, - is implemented in terms of -=, etc.

According to our rules of thumb, + and its companions should be non-members, while their compound assignment counterparts (+=, etc.), changing their left argument, should be a member. Here is the exemplary code for += and +; the other binary arithmetic operators should be implemented in the same way:

struct X {
  X& operator+=(const X& rhs)
  {
    // actual addition of rhs to *this
    return *this;
  }
};

inline X operator+(const X& lhs, const X& rhs)
{
  X result = lhs;
  result += rhs;
  return result;
}

operator+= returns its result per reference, while operator+ returns a copy of its result. Of course, returning a reference is usually more efficient than returning a copy, but in the case of operator+, there is no way around the copying. When you write a + b, you expect the result to be a new value, which is why operator+ has to return a new value.1

Also note that operator+ can be slightly shortened by passing lhs by value, not by reference.
However, this would be leaking implementation details, make the function signature asymmetric, and would prevent named return value optimization where result is the same object as the one being returned.

Sometimes, it's impractical to implement @ in terms of @=, such as for matrix multiplication.
In that case, you can also delegate @= to @:

struct Matrix {
  // You can also define non-member functions inside the class, i.e. "hidden friends"
  friend Matrix operator*(const Matrix& lhs, const Matrix& rhs) {
    Matrix result;
    // Do matrix multiplication
    return result;
  }
  Matrix& operator*=(const Matrix& rhs)
  {
    return *this = *this * rhs; // Assuming operator= returns a reference
  }
};

The bit manipulation operators ~ & | ^ << >> should be implemented in the same way as the arithmetic operators. However, (except for overloading << and >> for output and input) there are very few reasonable use cases for overloading these.

1 Again, the lesson to be taken from this is that a += b is, in general, more efficient than a + b and should be preferred if possible.

Subscript Operator

The subscript operator is a binary operator which must be implemented as a class member. It is used for container-like types that allow access to their data elements by a key.
The canonical form of providing these is this:

struct X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) const;
  // ...
};

Unless you do not want users of your class to be able to change data elements returned by operator[] (in which case you can omit the non-const variant), you should always provide both variants of the operator.

Operators for Pointer-like Types

For defining your own iterators or smart pointers, you have to overload the unary prefix dereference operator * and the binary infix pointer member access operator ->:

struct my_ptr {
        value_type& operator*();
  const value_type& operator*() const;
        value_type* operator->();
  const value_type* operator->() const;
};

Note that these, too, will almost always need both a const and a non-const version.
For the -> operator, if value_type is of class (or struct or union) type, another operator->() is called recursively, until an operator->() returns a value of non-class type.

The unary address-of operator should never be overloaded.

For operator->*() (and more details about operator->) see this question. operator->*() is rarely used and thus rarely ever overloaded. In fact, even iterators do not overload it.


Continue to Conversion Operators.

带刺的爱情 2025-01-22 23:14:27

C++ 中运算符重载的三个基本规则

当涉及到 C++ 中的运算符重载时,您应该遵循三个基本规则。与所有此类规则一样,确实也有例外。有时人们偏离了它们,结果也不是糟糕的代码,但这种积极的偏离是很少见的。至少,我见过的 100 个此类偏差中有 99 个都是不合理的。然而,它也可能是 1000 中的 999。所以你最好遵守以下规则。

  1. 当运算符的含义不明显且无可争议时,不应重载它。 相反,应提供一个具有精心选择的名称的函数.
    基本上,重载运算符的首要规则的核心是:不要这样做。这可能看起来很奇怪,因为关于运算符重载有很多东西需要了解,所以很多文章、书籍章节和其他文本都涉及所有这些。但是,尽管有这些看似明显的证据,只有极少数情况下运算符重载是合适的。原因在于,实际上很难理解运算符应用背后的语义,除非运算符在应用领域中的使用是众所周知的且无可争议的。与普遍看法相反,事实并非如此。

  2. 始终坚持运算符众所周知的语义。
    C++ 对重载运算符的语义没有限制。您的编译器会很乐意接受实现二进制 + 运算符的代码,以从其右操作数中减去。但是,此类运算符的用户绝不会怀疑表达式 a + b 会从 b 中减去 a。当然,这是假设应用程序域中运算符的语义是无可争议的。

  3. 始终提供一组相关操作中的所有操作。
    操作符彼此相关并与其他操作相关。如果您的类型支持 a + b,用户也希望能够调用 a += b。如果它支持前缀增量 ++a,他们会期望 a++ 也能工作。如果他们可以检查是否 a < b,他们肯定希望也能够检查 a > > b。如果他们可以复制构造您的类型,他们期望赋值也能工作。



继续会员与非会员之间的决定

The Three Basic Rules of Operator Overloading in C++

When it comes to operator overloading in C++, there are three basic rules you should follow. As with all such rules, there are indeed exceptions. Sometimes people have deviated from them and the outcome was not bad code, but such positive deviations are few and far between. At the very least, 99 out of 100 such deviations I have seen were unjustified. However, it might just as well have been 999 out of 1000. So you’d better stick to the following rules.

  1. Whenever the meaning of an operator is not obviously clear and undisputed, it should not be overloaded. Instead, provide a function with a well-chosen name.
    Basically, the first and foremost rule for overloading operators, at its very heart, says: Don’t do it. That might seem strange, because there is a lot to be known about operator overloading and so a lot of articles, book chapters, and other texts deal with all this. But despite this seemingly obvious evidence, there are only a surprisingly few cases where operator overloading is appropriate. The reason is that actually it is hard to understand the semantics behind the application of an operator unless the use of the operator in the application domain is well known and undisputed. Contrary to popular belief, this is hardly ever the case.

  2. Always stick to the operator’s well-known semantics.
    C++ poses no limitations on the semantics of overloaded operators. Your compiler will happily accept code that implements the binary + operator to subtract from its right operand. However, the users of such an operator would never suspect the expression a + b to subtract a from b. Of course, this supposes that the semantics of the operator in the application domain is undisputed.

  3. Always provide all out of a set of related operations.
    Operators are related to each other and to other operations. If your type supports a + b, users will expect to be able to call a += b, too. If it supports prefix increment ++a, they will expect a++ to work as well. If they can check whether a < b, they will most certainly expect to also to be able to check whether a > b. If they can copy-construct your type, they expect assignment to work as well.


Continue to The Decision between Member and Non-member.

何以畏孤独 2025-01-22 23:14:27

成员和非成员

类别 运算符 之间的决策决策
强制成员函数 []()=->< /code>, ... 成员函数(由 C++ 标准强制)
指向成员的指针访问 ->* 成员函数
一元 ++, - ,<代码>*new, ... 成员函数,枚举除外
复合赋值 +=, |=, *=, ... 成员函数,枚举除外
其他运算符 +==<=>/, ... 更喜欢非成员

二元运算符 = (赋值), [](数组订阅)、->(成员访问)以及n元()(函数调用)运算符,必须始终实现为成员函数,因为语言的语法要求它们这样做。

其他操作员可以作为成员或非成员来实现。然而,其中一些通常必须作为非成员函数实现,因为它们的左操作数无法由您修改。其中最突出的是输入和输出运算符 <<>>,它们的左操作数是标准库中的流类,您无法更改它们。

对于必须选择将其实现为成员函数或非成员函数的所有运算符,使用以下经验法则来决定:

  1. 如果是一元运算符,将其实现为成员函数。
  2. 如果二元运算符同等对待两个操作数(保持它们不变),则将此运算符实现为非成员 > 功能。
  3. 如果二元运算符对待它的两个操作数同等(通常它会改变它的左操作数),如果它必须访问操作数的私有部分,则使其成为其左操作数类型的成员函数可能会很有用。

当然,与所有经验法则一样,也有例外。如果您有一个类型

enum Month {Jan, Feb, ..., Nov, Dec}

并且想要重载它的自增和自减运算符,则不能将其作为成员函数执行此操作,因为在 C++ 中,枚举类型不能具有成员函数。所以你必须将它作为一个自由函数重载。当作为类定义中内联的成员函数完成时,嵌套在类模板中的类模板的运算符 <() 更容易编写和读取。但这些确实是罕见的例外。

(但是,如果你犯了一个例外,请不要忘记操作数的const性质问题,对于成员函数来说,它会成为隐式的this 最后使 *this 成为const 参考。)


继续要重载的常用运算符

The Decision between Member and Non-member

Category Operators Decision
Mandatory member functions [], (), =, ->, ... member function (mandated by the C++ standard)
Pointer-to-member access ->* member function
Unary ++, -, *, new, ... member function, except for enumerations
Compound assignment +=, |=, *=, ... member function, except for enumerations
Other operators +, ==, <=>, /, ... prefer non-member

The binary operators = (assignment), [] (array subscription), -> (member access), as well as the n-ary () (function call) operator, must always be implemented as member functions, because the syntax of the language requires them to.

Other operators can be implemented either as members or as non-members. Some of them, however, usually have to be implemented as non-member functions, because their left operand cannot be modified by you. The most prominent of these are the input and output operators << and >>, whose left operands are stream classes from the standard library which you cannot change.

For all operators where you have to choose to either implement them as a member function or a non-member function, use the following rules of thumb to decide:

  1. If it is a unary operator, implement it as a member function.
  2. If a binary operator treats both operands equally (it leaves them unchanged), implement this operator as a non-member function.
  3. If a binary operator does not treat both of its operands equally (usually it will change its left operand), it might be useful to make it a member function of its left operand’s type, if it has to access the operand's private parts.

Of course, as with all rules of thumb, there are exceptions. If you have a type

enum Month {Jan, Feb, ..., Nov, Dec}

and you want to overload the increment and decrement operators for it, you cannot do this as a member functions, since in C++, enum types cannot have member functions. So you have to overload it as a free function. And operator<() for a class template nested within a class template is much easier to write and read when done as a member function inline in the class definition. But these are indeed rare exceptions.

(However, if you make an exception, do not forget the issue of const-ness for the operand that, for member functions, becomes the implicit this argument. If the operator as a non-member function would take its left-most argument as a const reference, the same operator as a member function needs to have a const at the end to make *this a const reference.)


Continue to Common operators to overload.

狼亦尘 2025-01-22 23:14:27

C++ 中运算符重载的一般语法

您无法更改 C++ 中内置类型的运算符的含义,只能为用户定义类型重载运算符1。也就是说,至少一个操作数必须是用户定义的类型。与其他重载函数一样,特定参数集的运算符只能重载一次。

并非所有运算符都可以在 C++ 中重载。不能重载的运算符包括: . :: sizeof typeid .* 以及 C++ 中唯一的三元运算符 ?:

C++ 中可以重载的运算符包括:

类别 运算符 数量和位置
算术 + <代码>- * / %+= -= *= /= %= 二进制中缀
+ - 一元前缀
++ -- 一元前缀和后缀
按位 & | ^ << >>&= |= ^= <<= >>= 二进制中缀
~ 一元前缀
比较 == != < > <= >= <=> 二进制中缀
逻辑 | | && 二进制中缀
! 一元前缀
分配函数 new new[] delete delete[ ] 一元前缀
用户定义的转换 T 一元
赋值 = 二进制中缀
成员访问 -> ->* 二进制中缀
间接/地址 * & 一元前缀
函数调用< /strong> () N-ary 后缀
下标 [] N-ary2 postfix
协程等待 co_await 一元前缀
逗号 , 二进制中缀

然而,您可以重载所有这些并不意味着您可以应该这样做。请参阅下一个答案

在 C++ 中,运算符以具有特殊名称的函数的形式重载。与其他函数一样,重载运算符通常可以实现为其左操作数类型的成员函数非成员函数。您是否可以自由选择或必须使用其中之一取决于几个条件。3 一元运算符 @4,应用于对象 x ,作为 operator@(x)x.operator@() 调用。应用于对象 xy 的二元中缀运算符 @ 被称为 operator@(x,y)< /code> 或作为 x.operator@(y).5

作为非成员函数实现的运算符有时是其操作数类型的友元。

1 术语“用户定义”可能有点误导。 C++ 区分内置类型和用户定义类型。例如 int、char 和 double 属于前者;后者属于所有结构、类、联合和枚举类型,包括标准库中的类型,即使它们本身不是由用户定义的。

2 下标运算符过去是二元的,而不是 N 元,直到 C++23。

3 这在 本常见问题解答的后面部分

4 @ 不是 C++ 中的有效运算符,这就是我将其用作占位符的原因。

5 中唯一的三元运算符C++ 无法重载,并且唯一的 n 元运算符必须始终作为成员函数实现。


继续C++ 中运算符重载的三个基本规则

The General Syntax of operator overloading in C++

You cannot change the meaning of operators for built-in types in C++, operators can only be overloaded for user-defined types1. That is, at least one of the operands has to be of a user-defined type. As with other overloaded functions, operators can be overloaded for a certain set of parameters only once.

Not all operators can be overloaded in C++. Among the operators that cannot be overloaded are: . :: sizeof typeid .* and the only ternary operator in C++, ?:

Among the operators that can be overloaded in C++ are these:

Category Operators Arity and Placement
Arithmetic + - * / % and += -= *= /= %= binary infix
+ - unary prefix
++ -- unary prefix and postfix
Bitwise & | ^ << >> and &= |= ^= <<= >>= binary infix
~ unary prefix
Comparison == != < > <= >= <=> binary infix
Logical || && binary infix
! unary prefix
Allocation functions new new[] delete delete[] unary prefix
User-defined conversions T unary
Assignment = binary infix
Member access -> ->* binary infix
Indirection/Address-of * & unary prefix
Function call () N-ary postfix
Subscript [] N-ary2 postfix
Coroutine await co_await unary prefix
Comma , binary infix

However, the fact that you can overload all of these does not mean you should do so. See the next answer.

In C++, operators are overloaded in the form of functions with special names. As with other functions, overloaded operators can generally be implemented either as a member function of their left operand's type or as non-member functions. Whether you are free to choose or bound to use either one depends on several criteria.3 A unary operator @4, applied to an object x, is invoked either as operator@(x) or as x.operator@(). A binary infix operator @, applied to the objects x and y, is called either as operator@(x,y) or as x.operator@(y).5

Operators that are implemented as non-member functions are sometimes friend of their operand’s type.

1 The term “user-defined” might be slightly misleading. C++ makes the distinction between built-in types and user-defined types. To the former belong for example int, char, and double; to the latter belong all struct, class, union, and enum types, including those from the standard library, even though they are not, as such, defined by users.

2 The subscript operator used to be binary, not N-ary until C++23.

3 This is covered in a later part of this FAQ.

4 The @ is not a valid operator in C++ which is why I use it as a placeholder.

5 The only ternary operator in C++ cannot be overloaded and the only n-ary operator must always be implemented as a member function.


Continue to The Three Basic Rules of Operator Overloading in C++.

辞慾 2025-01-22 23:14:27

转换运算符(也称为用户定义的转换)

在 C++ 中,您可以创建转换运算符,这些运算符允许编译器在您的类型和其他定义的类型之间进行转换。有两种类型的转换运算符:隐式转换运算符和显式转换运算符。

隐式转换运算符(C++98/C++03 和 C++11)

隐式转换运算符允许编译器隐式转换(如 intlong 之间的转换) >) 将用户定义类型的值转换为其他类型。

下面是一个带有隐式转换运算符的简单类:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

隐式转换运算符与单参数构造函数一样,是用户定义的转换。当尝试匹配对重载函数的调用时,编译器将授予一个用户定义的转换。

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

乍一看,这似乎非常有帮助,但这样做的问题是,隐式转换甚至会在不期望的情况下启动。在以下代码中,将调用 void f(const char*) 因为 my_string() 不是 lvalue,因此第一个不匹配:

void f(my_string&);
void f(const char*);

f(my_string());

初学者很容易犯这个错误,甚至经验丰富的 C++ 程序员有时也会犯这个错误很惊讶,因为编译器选择了他们没有怀疑的重载。这些问题可以通过显式转换运算符来缓解。

显式转换运算符 (C++11)

与隐式转换运算符不同,显式转换运算符绝不会在您不期望的情况下启动。下面是一个带有显式转换运算符的简单类:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

注意显式。现在,当您尝试从隐式转换运算符执行意外代码时,您会收到编译器错误:

prog.cpp: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘const char*’

要调用显式强制转换运算符,您必须使用 static_cast、C 风格强制转换或构造函数样式转换(即 T(value) )。

但是,有一个例外:允许编译器隐式转换为 bool。此外,编译器在转换为 bool 后不允许再进行隐式转换(允许编译器一次进行 2 次隐式转换,但最多只能进行 1 次用户定义的转换)。

由于编译器不会强制转换“过去”bool,因此显式转换运算符现在不再需要 安全布尔习语。例如,C++11 之前的智能指针使用 Safe Bool 习惯用法来防止转换为整型。在 C++11 中,智能指针使用显式运算符,因为编译器在将类型显式转换为 bool 后不允许隐式转换为整型。

继续重载newdelete

Conversion Operators (also known as User Defined Conversions)

In C++ you can create conversion operators, operators that allow the compiler to convert between your types and other defined types. There are two types of conversion operators, implicit and explicit ones.

Implicit Conversion Operators (C++98/C++03 and C++11)

An implicit conversion operator allows the compiler to implicitly convert (like the conversion between int and long) the value of a user-defined type to some other type.

The following is a simple class with an implicit conversion operator:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

Implicit conversion operators, like one-argument constructors, are user-defined conversions. Compilers will grant one user-defined conversion when trying to match a call to an overloaded function.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

At first this seems very helpful, but the problem with this is that the implicit conversion even kicks in when it isn’t expected to. In the following code, void f(const char*) will be called because my_string() is not an lvalue, so the first does not match:

void f(my_string&);
void f(const char*);

f(my_string());

Beginners easily get this wrong and even experienced C++ programmers are sometimes surprised because the compiler picks an overload they didn’t suspect. These problems can be mitigated by explicit conversion operators.

Explicit Conversion Operators (C++11)

Unlike implicit conversion operators, explicit conversion operators will never kick in when you don't expect them to. The following is a simple class with an explicit conversion operator:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

Notice the explicit. Now when you try to execute the unexpected code from the implicit conversion operators, you get a compiler error:

prog.cpp: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘const char*’

To invoke the explicit cast operator, you have to use static_cast, a C-style cast, or a constructor style cast ( i.e. T(value) ).

However, there is one exception to this: The compiler is allowed to implicitly convert to bool. In addition, the compiler is not allowed to do another implicit conversion after it converts to bool (a compiler is allowed to do 2 implicit conversions at a time, but only 1 user-defined conversion at max).

Because the compiler will not cast "past" bool, explicit conversion operators now remove the need for the Safe Bool idiom. For example, smart pointers before C++11 used the Safe Bool idiom to prevent conversions to integral types. In C++11, the smart pointers use an explicit operator instead because the compiler is not allowed to implicitly convert to an integral type after it explicitly converted a type to bool.

Continue to Overloading new and delete.

缺⑴份安定 2025-01-22 23:14:27

重载 newdelete 运算符

注意:这仅处理语法重载newdelete,而不是实现此类重载运算符。我认为重载 newdelete 的语义值得他们自己的常见问题解答,在运算符重载主题中我永远无法公正地表达。

基础知识

在C++中,当您编写新表达式< /strong> 就像新的T(arg) 计算此表达式时会发生两件事:首先调用 operator new 来获取原始内存,然后调用 T 的适当构造函数将此原始内存转换为有效对象。同样,当您删除一个对象时,首先调用其析构函数,然后将内存返回给operator delete
C++ 允许您调整这两个操作:内存管理和在分配的内存中构造/销毁对象。后者是通过为类编写构造函数和析构函数来完成的。微调内存管理是通过编写您自己的operator newoperator delete 来完成的。

运算符重载的第一条基本规则 - 不要这样做 - 特别适用于重载 newdelete。几乎导致这些运算符重载的唯一原因是性能问题内存限制,并且在许多情况下,还有其他操作与所使用的算法的更改一样,将比尝试调整内存管理提供更高的成本/增益比

C++ 标准库附带了一组预定义的 newdelete 运算符。最重要的是:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

前两个为对象分配/释放内存,后两个为对象数组分配/释放内存。如果您提供自己的版本,它们将不会重载,而是替换标准库中的版本。
如果您重载operator new,您也应该始终重载匹配的operator delete,即使您从未打算调用它。原因是,如果构造函数在计算 new 表达式时抛出异常,运行时系统会将内存返回给与之前的 operator new 相匹配的 operator delete 。调用来分配内存以在其中创建对象。如果您没有提供匹配的删除运算符,则会调用默认运算符,这几乎总是错误的。
如果您重载 newdelete,您也应该考虑重载数组变体。

放置 new

C++ 允许 new 和 delete 运算符采用额外的参数。
所谓的放置新允许您在某个地址创建一个对象,该对象将被传递到:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

标准库为此提供了适当的 new 和删除运算符重载:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

请注意,在上面给出的放置新的示例代码中, operator delete 永远不会被调用,除非 X 的构造函数抛出异常。

您还可以使用其他参数重载 newdelete。与放置 new 的附加参数一样,这些参数也列在关键字 new 后面的括号内。仅出于历史原因,此类变体通常也称为新放置,即使它们的参数不是将对象放置在特定地址。

特定于类的 new 和删除

最常见的是,您需要微调内存管理,因为测量表明特定类或一组相关类的实例经常被创建和销毁,并且运行的默认内存管理-针对一般性能进行调整的时间系统在这种特定情况下处理效率低下。为了改善这一点,您可以为特定类重载 new 和 delete:

class my_class { 
  public: 
    // ... 
    void* operator new(std::size_t);
    void  operator delete(void*);
    void* operator new[](std::size_t);
    void  operator delete[](void*);
    // ...  
}; 

因此重载后,new 和 delete 的行为类似于静态成员函数。对于 my_class 的对象,std::size_t 参数将始终为 sizeof(my_class)。但是,这些运算符也会被动态分配的派生类对象调用,在这种情况下,它可能会大于此值。

全局new和delete

要重载全局new和delete,只需用我们自己的替换标准库的预定义运算符即可。然而,很少需要这样做。

Overloading new and delete operators

Note: This only deals with the syntax of overloading new and delete, not with the implementation of such overloaded operators. I think that the semantics of overloading new and delete deserve their own FAQ, within the topic of operator overloading I can never do it justice.

Basics

In C++, when you write a new expression like new T(arg) two things happen when this expression is evaluated: First operator new is invoked to obtain raw memory, and then the appropriate constructor of T is invoked to turn this raw memory into a valid object. Likewise, when you delete an object, first its destructor is called, and then the memory is returned to operator delete.
C++ allows you to tune both of these operations: memory management and the construction/destruction of the object at the allocated memory. The latter is done by writing constructors and destructors for a class. Fine-tuning memory management is done by writing your own operator new and operator delete.

The first of the basic rules of operator overloading – don’t do it – applies especially to overloading new and delete. Almost the only reasons to overload these operators are performance problems and memory constraints, and in many cases, other actions, like changes to the algorithms used, will provide a much higher cost/gain ratio than attempting to tweak memory management.

The C++ standard library comes with a set of predefined new and delete operators. The most important ones are these:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

The first two allocate/deallocate memory for an object, the latter two for an array of objects. If you provide your own versions of these, they will not overload, but replace the ones from the standard library.
If you overload operator new, you should always also overload the matching operator delete, even if you never intend to call it. The reason is that, if a constructor throws during the evaluation of a new expression, the run-time system will return the memory to the operator delete matching the operator new that was called to allocate the memory to create the object in. If you do not provide a matching operator delete, the default one is called, which is almost always wrong.
If you overload new and delete, you should consider overloading the array variants, too.

Placement new

C++ allows new and delete operators to take additional arguments.
So-called placement new allows you to create an object at a certain address which is passed to:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

The standard library comes with the appropriate overloads of the new and delete operators for this:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

Note that, in the example code for placement new given above, operator delete is never called, unless the constructor of X throws an exception.

You can also overload new and delete with other arguments. As with the additional argument for placement new, these arguments are also listed within parentheses after the keyword new. Merely for historical reasons, such variants are often also called placement new, even if their arguments are not for placing an object at a specific address.

Class-specific new and delete

Most commonly you will want to fine-tune memory management because measurement has shown that instances of a specific class, or of a group of related classes, are created and destroyed often and that the default memory management of the run-time system, tuned for general performance, deals inefficiently in this specific case. To improve this, you can overload new and delete for a specific class:

class my_class { 
  public: 
    // ... 
    void* operator new(std::size_t);
    void  operator delete(void*);
    void* operator new[](std::size_t);
    void  operator delete[](void*);
    // ...  
}; 

Overloaded thus, new and delete behave like static member functions. For objects of my_class, the std::size_t argument will always be sizeof(my_class). However, these operators are also called for dynamically allocated objects of derived classes, in which case it might be greater than that.

Global new and delete

To overload the global new and delete, simply replace the pre-defined operators of the standard library with our own. However, this rarely ever needs to be done.

半世蒼涼 2025-01-22 23:14:27

为什么operator<<函数不能用于流式传输对象std::cout 还是将文件作为成员函数?

您有:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

鉴于此,您不能使用:

Foo f = {10, 20.0};
std::cout << f;

假设 operator<< 被重载为 Foo 的成员函数,该运算符的 LHS 必须是 Foo 对象。这意味着,您将需要使用:

Foo f = {10, 20.0};
f << std::cout

这是非常不直观的。

如果将其定义为非成员函数,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

您将能够使用:,

Foo f = {10, 20.0};
std::cout << f;

这非常直观。

Why can't operator<< function for streaming objects to std::cout or to a file be a member function?

Let's say you have:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

Given that, you cannot use:

Foo f = {10, 20.0};
std::cout << f;

Since operator<< is overloaded as a member function of Foo, the LHS of the operator must be a Foo object. Which means, you will be required to use:

Foo f = {10, 20.0};
f << std::cout

which is very non-intuitive.

If you define it as a non-member function,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

You will be able to use:

Foo f = {10, 20.0};
std::cout << f;

which is very intuitive.

凉宸 2025-01-22 23:14:27

比较运算符,包括三向比较(C++20)

相等比较 ==!=,
关系比较 <><=>=
C++20 还引入了三向比较运算符<=>

运算 符含义和注释(旧) 含义和注释 (C++20)
x == y 如果 xy 相等则为 true

满足EqualityComparable
(由 std::unordered_map 使用)

如果 xy 为 true equal
(可以实现为 (x <=> y) == 0,
但通常不是)

满足 std::equality_comparable

x != y !(x == y) !(x == y)
x y 如果 x 低于 y,则 true

满足 LessThanComparable
(由 std::set 使用, std::sort
但需要严格的弱排序)

(x <=> y) < 0

可能满足std::strict_weak_ordering
当包装在仿函数中时
(例如std::ranges::less

x > y y < x (x <=> y) > 0
x <= y !(x < y) 对于强排序,
x == y || x < y 否则
(x <=> y) <= 0
x >= y y <= x (x <=> y) >= 0
x <=> y N/A 三向比较
又名。 “太空飞船操作员”

满足 std:: Three_way_comparable

准则

  1. 比较运算符不应该是成员函数。1)
  2. 如果定义 ==,请定义!= 也是如此(除非用 C++20 重写)。
  3. 如果定义 <,请同时定义 ><=>=
  4. (C++20) 优先定义 <=> 而不是定义每个关系运算符。
  5. (C++20) 优先选择默认运算符而不是手动实现。
  6. 相等和关系比较应该匹配,这意味着
    x == y 应等于 !(x < y) && !(y < x)2)
  7. 不要用 < 定义 ==,即使你可以3)

1) 否则,隐式转换将是不对称的,并且 == 预计会将相同类型的隐式转换应用于双方。
2) 这种等价不适用于float,但适用于int和其他强有序类型。< br>
3) 这是出于可读性、正确性和性能的考虑。

C++20 之前的实现和常见习惯

用法免责声明
如果您使用的是 C++20,则实现本节中的内容已过时。
除非您对历史观点感兴趣,否则请跳至 C++20 部分。

所有运算符通常都实现为非成员函数,可能作为隐藏好友 ( friend 是在类内部定义函数的位置)。
以下所有代码示例都使用隐藏的朋友,因为如果您无论如何都需要比较私有成员,则这是必要的。

struct S {
    int x, y, z;

    // (In)equality comparison:
    // implementing a member-wise equality
    friend bool operator==(const S& l, const S& r) {
        return l.x == r.x && l.y == r.y && l.z == r.z;
    }
    friend bool operator!=(const S& l, const S& r) { return !(l == r); }

    // Relational comparisons:
    // implementing a lexicographical comparison which induces a
    // strict weak ordering.
    friend bool operator<(const S& l, const S& r) {
        if (l.x < r.x) return true;   // notice how all sub-comparisons
        if (r.x < l.x) return false;  // are implemented in terms of <
        if (l.y < r.y) return true;
        if (r.y < l.y) return false; // also see below for a possibly simpler
        return l.z < r.z;            // implementation
    }
    friend bool operator>(const S& l, const S& r) { return r < l; }
    friend bool operator<=(const S& l, const S& r) { return !(r < l); }
    friend bool operator>=(const S& l, const S& r) { return !(l < r); }
};

注意:在 C++11 中,所有这些通常可以是 noexceptconstexpr

根据 < 实现所有关系比较如果我们有部分有序成员(例如 float),则 ; 无效。
在这种情况下,<=>= 必须以不同的方式编写。

friend bool operator<=(const S& l, const S& r) { return l == r || l < r; }
friend bool operator>=(const S& l, const S& r) { return r <= l; }

关于operator<的进一步说明

operator<的实现并不那么简单,因为正确的字典比较不能简单地比较每个成员一次。
<代码>{1, 2} < {3, 0} 应该为 true,即使 2 0 为假。

字典比较是实现严格弱排序的简单方法,这是需要的像 std::set 这样的容器和像 std::sort 这样的算法。简而言之,严格弱排序应该像 < 运算符用于整数,但某些整数允许相等(例如,对于所有偶数整数,x < y 为 false)。

如果 x != y 相当于 x y || y < x,更简单的方法是可能的:

friend bool operator<(const S& l, const S& r) {
    if (l.x != r.x) return l.x < r.x;
    if (l.y != r.y) return l.y < r.y;
    return l.z < r.z;
}

常见习惯用法

对于多个成员,您可以使用 std::tie 按字典顺序实现比较:

#include <tuple>

struct S {
    int x, y, z;

    friend bool operator<(const S& l, const S& r) {
        return std::tie(l.x, l.y, l.z) < std::tie(r.x, r.y, r.z);
    }
};

使用 std::lexicographyal_compare 用于数组成员。

有些人使用宏或奇怪的重复模板模式(CRTP)来保存委托 !=>>= 的样板,和 <=,或者模仿 C++20 的三向比较。

也可以使用 std::rel_ops< /a>(在 C++20 中已弃用)委托 !=><= 和对于某个范围内的所有类型,>=<==


默认比较 (C++20)

大量比较运算符只是比较类的每个成员。
如果是这样,那么实现就是纯粹的样板文件,我们可以让编译器完成这一切:

struct S {
    int x, y, z;
    // ==, !=, <, >, <=, >= are all defined.
    // constexpr and noexcept are inferred automatically.
    friend auto operator<=>(const S&, const S&) = default;
};

注意:默认的比较运算符需要是类的友元,实现这一点的最简单方法是通过将它们定义为类中的默认值。这使它们成为“隐藏的朋友”。

或者,我们可以默认单独的比较运算符。
如果我们想定义相等比较或仅定义关系比较,这非常有用:

friend bool operator==(const S&, const S&) = default; // inside S

请参阅关于默认比较的 cppreference 文章

表达式重写 (C++20)

在 C++20 中,如果比较运算符不是用户声明的,编译器还将尝试使用重写候选者。
因此,即使 <=> 未默认(这将实现所有运算符),我们也只需实现 ==<=>,所有其他比较都根据这两者重写。

运算 符势重写
x == y y == x
x != y !(x == y)!(y == x) 如果相等比较返回 bool
x y (x <=> y) < 00 (y <=> x) 如果比较结果与零相当
x > y (x <=> y) > 00 > (y <=> x) if ...
x <= y (x <=> y) <= 00 <= (y <=> x) if ...
x >= y (x <=> y) > = 00 >= (y <=> x) if ...
struct S {
    int x, y, z;
    // ==, !=
    friend constexpr bool operator==(const S& l, const S& r) noexcept { /* ... */ }
    // <=>, <, >, <=, >=
    friend constexpr auto operator<=>(const S& l, const S& r) noexcept { /* ... */ }
};

注意:constexprnoexcept 是可选的,但是几乎总是可以应用于比较运算符。

注意:在 P1185R2:<代码><=> != ==x == y 表达式也将被重写为 (x <=> y) == 0 ,但这效率很低,因为它错过了短路机会。

三向比较运算符 (C++20)

注意:它通俗地称为“太空船运算符”。另请参阅

背后的基本思想x <=>; y 的优点是结果告诉我们 x 是否小于、大于、等于或与 y 无序。
这类似于 C 中的 strcmp 等函数。

// old C style
int compare(int x, int y) {
    if (x < y) return -1;
    if (x > y) return  1;
    return             0; // or simply return (x > y) - (x < y);
}
// C++20 style: this is what <=> does for int.
auto compare_cxx20(int x, int y) {
    if (x < y) return std::strong_ordering::less;
    if (x > y) return std::strong_ordering::greater;
    return            std::strong_ordering::equal;
}
// This is what <=> does for float.
auto compare_cxx20(float x, float y) {
    if (x < y)  return std::partial_ordering::less;
    if (x > y)  return std::partial_ordering::greater;
    if (x == y) return std::partial_ordering::equivalent;
    return             std::partial_ordering::unordered; // NaN
}

比较类别

该运算符的结果既不是 bool 也不是 int ,而是比较类别的值。

比较类别 示例 可能的值
std::strong_ordering int 更少等于=等价更大
std::weak_ordering 用户定义1) 更少等价更大
std::partial_ordering 浮动< /code> 更少等价更大无序

std::strong_ordering可以转换为std::weak_ordering,后者可以转换为std::partial_ordering
这些类别的值可与(例如 (x <=> y) == 0)进行比较,这与上面的 compare 函数具有类似的含义。
但是,std::partial_ordering::unordered 对于所有比较都返回 false。


1) 没有任何基本类型可以满足 x <=> 的要求。 y 结果为 std::weak_ordering。强序和弱序在实践中是可以互换的;参见std::strong_ordering和std::weak_ordering的实际意义

手动实现三路比较

三-way 比较通常是默认的,但可以手动实现,例如:

#include <compare> // necessary, even if we don't use std::is_eq

struct S {
    int x, y, z;
    // This implementation is the same as what the compiler would do
    // if we defaulted <=> with = default;
    friend constexpr auto operator<=>(const S& l, const S& r) noexcept {
        // C++17 if statement with declaration makes this more readable.
        // !std::is_eq(c) is not the same as std::is_neq(c); it is also true
        // for std::partial_order::unordered.
        if (auto c = l.x <=> r.x; !std::is_eq(c)) /* 1) */ return c;
        if (auto c = l.y <=> r.y; !std::is_eq(c)) return c;
        return l.y <=> r.y;
    }
    // == is not automatically defined in terms of <=>.
    friend constexpr bool operator==(const S&, const S&) = default;
};

如果 S 的所有成员不是同一类型,则具有 auto 返回类型可能是一个问题因为比较类别(<=>的类型)可能不同,多个return语句的推导结果可能不一致。
在这种情况下,我们可以直接指定类别(在返回类型中),也可以使用 std::common_comparison_category

std::common_comparison_category_t<decltype(l.x <=> l.x), /* ... */>

1) 帮助函数如 std::is_neq 比较<=> 的结果为零。
它们更清楚地表达意图,但您不必使用它们。

常见习语

或者,我们可以让 std::tie 找出详细信息:

#include <tuple>

struct S {
    int x, y, z;

    friend constexpr auto operator<=>(const S& l, const S& r) noexcept {
        return std::tie(l.x, l.y, l.z) <=> std::tie(r.x, r.y, r.z);
    }
};

使用 std::lexicographical_compare_third_way 用于数组成员。

Comparison Operators, including Three-Way Comparison(C++20)

There are equality comparisons == and !=,
and relational comparisons <, >, <=, >=.
C++20 has also introduced the three-way comparison operator <=>.

Operator Meaning and Notes (Old) Meaning and Notes (C++20)
x == y true if x and y are equal

satisfies EqualityComparable
(used by std::unordered_map)

true if x and y are equal
(can be implemented as (x <=> y) == 0,
but usually isn't)

satisfies std::equality_comparable

x != y !(x == y) !(x == y)
x < y true if x is lower than y

satisfies LessThanComparable
(used by std::set, std::sort, etc.
but requires strict weak ordering)

(x <=> y) < 0

may satisfy std::strict_weak_ordering
when wrapped in a functor
(e.g. std::ranges::less)

x > y y < x (x <=> y) > 0
x <= y !(x < y) for strong orderings,
x == y || x < y otherwise
(x <=> y) <= 0
x >= y y <= x (x <=> y) >= 0
x <=> y N/A three-way comparison
aka. "spaceship operator"

satisfies std::three_way_comparable

Guidelines

  1. Comparison operators shouldn't be member functions.1)
  2. If you define ==, define != too (unless it is rewritten in C++20).
  3. If you define <, define >, <=, and >= too.
  4. (C++20) Prefer defining <=> over defining each relational operator.
  5. (C++20) Prefer defaulting operators over implementing manually.
  6. Equality and relational comparisons should match, meaning that
    x == y should be equivalent to !(x < y) && !(y < x)2)
  7. Don't define == in terms of <, even when you could 3)

1) Otherwise, implicit conversions would be asymmetrical, and == is expected to apply the same kinds of implicit conversions to both sides.
2) This equivalence does not apply to float, but does apply to int and other strongly ordered types.
3) This is motivated by readability, correctness, and performance.

Implementation and Common Idioms Prior to C++20

Disclaimer
If you're using C++20, the implementations in this section have been obsoleted.
Skip ahead to the C++20 parts unless you're interested in a historical perspective.

All operators are typically implemented as non-member functions, possibly as hidden friends (friends where the function is defined inside the class).
All following code examples use hidden friends because this becomes necessary if you need to compare private members anyway.

struct S {
    int x, y, z;

    // (In)equality comparison:
    // implementing a member-wise equality
    friend bool operator==(const S& l, const S& r) {
        return l.x == r.x && l.y == r.y && l.z == r.z;
    }
    friend bool operator!=(const S& l, const S& r) { return !(l == r); }

    // Relational comparisons:
    // implementing a lexicographical comparison which induces a
    // strict weak ordering.
    friend bool operator<(const S& l, const S& r) {
        if (l.x < r.x) return true;   // notice how all sub-comparisons
        if (r.x < l.x) return false;  // are implemented in terms of <
        if (l.y < r.y) return true;
        if (r.y < l.y) return false; // also see below for a possibly simpler
        return l.z < r.z;            // implementation
    }
    friend bool operator>(const S& l, const S& r) { return r < l; }
    friend bool operator<=(const S& l, const S& r) { return !(r < l); }
    friend bool operator>=(const S& l, const S& r) { return !(l < r); }
};

Note: in C++11, all of these can typically be noexcept and constexpr.

Implementing all relational comparisons in terms of < is not valid if we have a partially ordered member (e.g. float).
In that case, <= and >= must be written differently.

friend bool operator<=(const S& l, const S& r) { return l == r || l < r; }
friend bool operator>=(const S& l, const S& r) { return r <= l; }

Further Notes on operator<

The implementation of operator< is not so simple because a proper lexicographical comparison cannot simply compare each member once.
{1, 2} < {3, 0} should be true, even though 2 < 0 is false.

A lexicographical comparison is a simple way of implementing a strict weak ordering, which is needed for containers like std::set and algorithms like std::sort. In short, a strict weak ordering should behave like the < operator for integers, except that some integers are allowed to be equivalent (e.g. for all even integers, x < y is false).

If x != y is equivalent to x < y || y < x, a simpler approach is possible:

friend bool operator<(const S& l, const S& r) {
    if (l.x != r.x) return l.x < r.x;
    if (l.y != r.y) return l.y < r.y;
    return l.z < r.z;
}

Common Idioms

For multiple members, you can use std::tie to implement comparison lexicographically:

#include <tuple>

struct S {
    int x, y, z;

    friend bool operator<(const S& l, const S& r) {
        return std::tie(l.x, l.y, l.z) < std::tie(r.x, r.y, r.z);
    }
};

Use std::lexicographical_compare for array members.

Some people use macros or the curiously recurring template pattern (CRTP) to save the boilerplate of delegating !=, >, >=, and <=, or to imitate C++20's three-way comparison.

It is also possible to use std::rel_ops (deprecated in C++20) to delegate !=, >, <=, and >= to < and == for all types in some scope.


Default Comparisons (C++20)

A substantial amount of comparison operators simply compare each member of a class.
If so, the implementation is pure boilerplate and we can let the compiler do it all:

struct S {
    int x, y, z;
    // ==, !=, <, >, <=, >= are all defined.
    // constexpr and noexcept are inferred automatically.
    friend auto operator<=>(const S&, const S&) = default;
};

Note: defaulted comparison operators need to be friends of the class, and the easiest way to accomplish that is by defining them as defaulted inside the class. This makes them "hidden friends".

Alternatively, we can default individual comparison operators.
This is useful if we want to define equality comparison, or only relational comparison:

friend bool operator==(const S&, const S&) = default; // inside S

See the cppreference article on default comparison.

Expression Rewriting (C++20)

In C++20, if a comparison operator isn't user-declared, the compiler will also try to use rewrite candidates.
Thanks to this, even if <=> isn't defaulted (which would implement all operators), we only have to implement == and <=>, and all other comparisons are rewritten in terms of these two.

Operator Potential Rewrites
x == y y == x
x != y !(x == y) or !(y == x) if equality comparison returns bool
x < y (x <=> y) < 0 or 0 < (y <=> x) if comparison result is comparable to zero
x > y (x <=> y) > 0 or 0 > (y <=> x) if ...
x <= y (x <=> y) <= 0 or 0 <= (y <=> x) if ...
x >= y (x <=> y) >= 0 or 0 >= (y <=> x) if ...
struct S {
    int x, y, z;
    // ==, !=
    friend constexpr bool operator==(const S& l, const S& r) noexcept { /* ... */ }
    // <=>, <, >, <=, >=
    friend constexpr auto operator<=>(const S& l, const S& r) noexcept { /* ... */ }
};

Note: constexpr and noexcept are optional, but can almost always be applied to comparison operators.

Note: Before P1185R2: <=> != ==, a x == y expression would also be rewritten as (x <=> y) == 0, but this would have been inefficient because it misses short-circuiting opportunities.

Three-Way Comparison Operator (C++20)

Note: it is colloquially called "spaceship operator". See also .

The basic idea behind x <=> y is that the result tells us whether x is lower than, greater than, equivalent to, or unordered with y.
This is similar to functions like strcmp in C.

// old C style
int compare(int x, int y) {
    if (x < y) return -1;
    if (x > y) return  1;
    return             0; // or simply return (x > y) - (x < y);
}
// C++20 style: this is what <=> does for int.
auto compare_cxx20(int x, int y) {
    if (x < y) return std::strong_ordering::less;
    if (x > y) return std::strong_ordering::greater;
    return            std::strong_ordering::equal;
}
// This is what <=> does for float.
auto compare_cxx20(float x, float y) {
    if (x < y)  return std::partial_ordering::less;
    if (x > y)  return std::partial_ordering::greater;
    if (x == y) return std::partial_ordering::equivalent;
    return             std::partial_ordering::unordered; // NaN
}

Comparison Categories

The result of this operator is neither bool nor int, but a value of comparison category.

Comparison Category Example Possible Values
std::strong_ordering int less, equal = equivalent, greater
std::weak_ordering user-defined1) less, equivalent, greater
std::partial_ordering float less, equivalent, greater, unordered

std::strong_orderings can be converted to std::weak_ordering, which can be converted to std::partial_ordering.
Values of these categories are comparable to (e.g. (x <=> y) == 0) and this has similar meaning to the compare function above.
However, std::partial_ordering::unordered returns false for all comparisons.


1) There are no fundamental types for which x <=> y results in std::weak_ordering. Strong and weak orderings are interchangeable in practice; see Practical meaning of std::strong_ordering and std::weak_ordering.

Manual Implementation of Three-Way Comparison

Three-way comparison is often defaulted, but could be implemented manually like:

#include <compare> // necessary, even if we don't use std::is_eq

struct S {
    int x, y, z;
    // This implementation is the same as what the compiler would do
    // if we defaulted <=> with = default;
    friend constexpr auto operator<=>(const S& l, const S& r) noexcept {
        // C++17 if statement with declaration makes this more readable.
        // !std::is_eq(c) is not the same as std::is_neq(c); it is also true
        // for std::partial_order::unordered.
        if (auto c = l.x <=> r.x; !std::is_eq(c)) /* 1) */ return c;
        if (auto c = l.y <=> r.y; !std::is_eq(c)) return c;
        return l.y <=> r.y;
    }
    // == is not automatically defined in terms of <=>.
    friend constexpr bool operator==(const S&, const S&) = default;
};

If all members of S weren't the same type, having an auto return type could be an issue because the comparison category (type of <=>) could be different and the multiple return statements would have inconsistent deduction.
In that case, we could either specify the category directly (in the return type), or we could obtain it with std::common_comparison_category:

std::common_comparison_category_t<decltype(l.x <=> l.x), /* ... */>

1) Helper functions like std::is_neq compare the result of <=> to zero.
They express intent more clearly, but you don't have to use them.

Common Idioms

Alternatively, we can let std::tie figure out the details:

#include <tuple>

struct S {
    int x, y, z;

    friend constexpr auto operator<=>(const S& l, const S& r) noexcept {
        return std::tie(l.x, l.y, l.z) <=> std::tie(r.x, r.y, r.z);
    }
};

Use std::lexicographical_compare_three_way for array members.

葬花如无物 2025-01-22 23:14:27

规范函数签名摘要

许多运算符重载几乎可以返回任何内容。例如,没有什么可以阻止您在 operator== 中返回 void
然而,这些签名中只有少数是规范的,这意味着您通常会这样编写它们,并且可以使用 = default 显式默认这样的运算符。

赋值运算符

struct X {
  X& operator=(const X&) = default;     // copy assignment operator
  X& operator=(X&&) noexcept = default; // move assignment operator
};

可以使用 = default; 显式默认,但您也可以手动实现赋值。
移动分配几乎总是 noexcept,尽管这不是强制性的。

比较运算符

#include <compare> // for comparison categories

struct X {
  friend auto operator<=>(const X&, const X&) = default; // defaulted three-way comparison
  friend std::strong_ordering<=>(const X&, const X&);    // manual three-way comparison

  friend bool operator==(const X&, const X&) = default;  // equality comparisons
  friend bool operator!=(const X&, const X&) = default;  // defaultable since C++20

  friend bool operator<(const X&, const X&) = default;   // relational comparisons
  friend bool operator>(const X&, const X&) = default;   // defaultable since C++20
  friend bool operator<=(const X&, const X&) = default;
  friend bool operator>=(const X&, const X&) = default;
};

有关何时以及如何默认/实现的更多信息,请参阅此答案比较。

算术运算符

struct X {
  friend X operator+(const X&, const X&); // binary plus
  friend X operator*(const X&, const X&); // binary multiplication
  friend X operator-(const X&, const X&); // binary minus
  friend X operator/(const X&, const X&); // binary division
  friend X operator%(const X&, const X&); // binary remainder

  X operator+() const;                    // unary plus
  X operator-() const;                    // unary minus

  X& operator++();                        // prefix increment
  X& operator--();                        // prefix decrement
  X  operator++(int);                     // postfix increment
  X  operator--(int);                     // postfix decrement

  X& operator+=(const X&);                // compound arithmetic assignment
  X& operator-=(const X&);
  X& operator*(const X&);
  X& operator/=(const X&);
  X& operator%=(const X&);
};

也可以按值获取二元运算符的左运算符,但不建议这样做,因为它会使签名不对称并抑制编译器优化。

按位运算符

struct X {
  using difference_type = /* some integer type */;

  friend X operator&(const X&, const X&);         // bitwise AND
  friend X operator|(const X&, const X&);         // bitwise OR
  friend X operator^(const X&, const X&);         // bitwise XOR
  
  friend X operator<<(const X&, difference_type); // bitwise left-shift
  friend X operator>>(const X&, difference_type); // bitwise right-shift

  X operator~() const;                            // bitwise NOT

  X& operator&=(const X&);                        // compound bitwise assignment
  X& operator|=(const X&);
  X& operator^(const X&);
  X& operator/=(const X&);
  X& operator%=(const X&);
};

流插入和提取

#include <ostream> // std::ostream
#include <istream> // std::istream

struct X {
  friend std::ostream& operator<<(std::ostream&, const X&); // stream insertion
  friend std::istream& operator>>(std::istream&, X&);       // stream extraction
};

函数调用运算符

struct X {
  using result = /* ... */;

         result operator()(user-defined-args...) /* const / volatile / & / && */;
  static result operator()(user-defined-args...);           // since C++23
};

下标运算符

struct X {
  using key_type = /* ... */;
  using value_type = /* ... */;

  const value_type& operator[](key_type) const;
        value_type& operator[](key_type);

  static value_type& operator[](key_type); // since C++23
};

请注意,自 C++23 起,operator[] 可以接受多个参数。

成员访问运算符

struct X {
  using value_type = /* ... */;

  const value_type& operator*() const; // indirection operator
        value_type& operator*();

  const value_type* operator->() const; // arrow operator
        value_type* operator->();
};

指向成员

struct X {
  using member_type = /* ... */;
  using member_pointer_type = /* ... */;
  
  const member_type& operator->*(member_pointer_type) const;
        member_type& operator->*(member_pointer_type);
};

的指针运算符 寻址运算符

struct X {
  using address_type = /* ... */;

  address_type operator&() const; // address-of operator
};

逻辑运算符

struct X {
  friend X operator&&(const X&, const X&); // logical AND
  friend X operator||(const X&, const X&); // logical OR
  friend X operator!(const X&);            // logical NOT
};

请注意,这些运算符不会返回 bool,因为它们仅在 X 已经是逻辑类型且满足以下条件时才有意义:与bool类似。

用户定义的转换

struct X {
  using type = /* ... */;

  operator type() const;          // arbitrary implicit conversion

  explicit operator bool() const; // explicit/contextual conversion to bool

  template <typename T>
    requires /* ... */            // optionally constrained
  explicit operator T() const;    // conversion function template
};

协程 Await

struct X {
  using awaiter = /* ... */;

  awaiter operator co_await() const;
};

逗号运算

struct X {
  using pair_type = /* ... */;

  // often a template to support combination of arbitrary types
  friend pair_type operator,(const X&, const X&);
};

符分配函数

struct X {
  // class-specific allocation functions
  void* operator new(std::size_t);
  void* operator new[](std::size_t);
  void* operator new(std::size_t, std::align_val_t); // C++17
  void* operator new[](std::size_t, std::align_val_t); // C++17

  // class-specific placement allocation functions
  void* operator new(std::size_t, user-defined-args...);
  void* operator new[](std::size_t, user-defined-args...);
  void* operator new(std::size_t, std::align_val_t, user-defined-args...); // C++17
  void* operator new[](std::size_t, std::align_val_t, user-defined-args...); // C++17

  // class-specific usual deallocation functions
  void operator delete(void*);
  void operator delete[](void*);
  void operator delete(void*, std::align_val_t); // C++17
  void operator delete[](void*, std::align_val_t); // C++17
  void operator delete(void*, std::size_t);
  void operator delete[](void*, std::size_t);
  void operator delete(void*, std::size_t, std::align_val_t); // C++17
  void operator delete[](void*, std::size_t, std::align_val_t); // C++17

  // class-specific placement deallocation functions
  void operator delete(void*, user-defined-args...);
  void operator delete(void*, user-defined-args...);

  // class-specific usual destroying deallocation functions
  void operator delete(X*, std::destroying_delete_t); // C++20
  void operator delete(X*, std::destroying_delete_t, std::align_val_t); // C++20
  void operator delete(X*, std::destroying_delete_t, std::size_t); // C++20
  void operator delete(X*, std::destroying_delete_t, std::size_t, std::align_val_t); // C++20
};

// non-class specific replaceable allocation functions ...

void* operator new(std::size_t);
void* operator delete(void*);
// ...

Summary of Canonical Function Signatures

Many operator overloads can return pretty much anything. For example, nothing stops you from returning void in operator==.
However, only a few of these signatures are canonical, which means that you would normally write them that way, and that such an operator can be explicitly defaulted with = default.

Assignment Operators

struct X {
  X& operator=(const X&) = default;     // copy assignment operator
  X& operator=(X&&) noexcept = default; // move assignment operator
};

Explicit defaulting with = default; is possible, but you can also implement assignment manually.
Move assignment is almost always noexcept, although it isn't mandatory.

Comparison Operators

#include <compare> // for comparison categories

struct X {
  friend auto operator<=>(const X&, const X&) = default; // defaulted three-way comparison
  friend std::strong_ordering<=>(const X&, const X&);    // manual three-way comparison

  friend bool operator==(const X&, const X&) = default;  // equality comparisons
  friend bool operator!=(const X&, const X&) = default;  // defaultable since C++20

  friend bool operator<(const X&, const X&) = default;   // relational comparisons
  friend bool operator>(const X&, const X&) = default;   // defaultable since C++20
  friend bool operator<=(const X&, const X&) = default;
  friend bool operator>=(const X&, const X&) = default;
};

See this answer for more information on when and how to default/implement comparisons.

Arithmetic Operators

struct X {
  friend X operator+(const X&, const X&); // binary plus
  friend X operator*(const X&, const X&); // binary multiplication
  friend X operator-(const X&, const X&); // binary minus
  friend X operator/(const X&, const X&); // binary division
  friend X operator%(const X&, const X&); // binary remainder

  X operator+() const;                    // unary plus
  X operator-() const;                    // unary minus

  X& operator++();                        // prefix increment
  X& operator--();                        // prefix decrement
  X  operator++(int);                     // postfix increment
  X  operator--(int);                     // postfix decrement

  X& operator+=(const X&);                // compound arithmetic assignment
  X& operator-=(const X&);
  X& operator*(const X&);
  X& operator/=(const X&);
  X& operator%=(const X&);
};

It is also possible to take the left operator of binary operators by value, but this is not recommended because it makes the signature asymmetric and inhibits compiler optimizations.

Bitwise Operators

struct X {
  using difference_type = /* some integer type */;

  friend X operator&(const X&, const X&);         // bitwise AND
  friend X operator|(const X&, const X&);         // bitwise OR
  friend X operator^(const X&, const X&);         // bitwise XOR
  
  friend X operator<<(const X&, difference_type); // bitwise left-shift
  friend X operator>>(const X&, difference_type); // bitwise right-shift

  X operator~() const;                            // bitwise NOT

  X& operator&=(const X&);                        // compound bitwise assignment
  X& operator|=(const X&);
  X& operator^(const X&);
  X& operator/=(const X&);
  X& operator%=(const X&);
};

Stream Insertion and Extraction

#include <ostream> // std::ostream
#include <istream> // std::istream

struct X {
  friend std::ostream& operator<<(std::ostream&, const X&); // stream insertion
  friend std::istream& operator>>(std::istream&, X&);       // stream extraction
};

Function Call Operator

struct X {
  using result = /* ... */;

         result operator()(user-defined-args...) /* const / volatile / & / && */;
  static result operator()(user-defined-args...);           // since C++23
};

Subscript Operator

struct X {
  using key_type = /* ... */;
  using value_type = /* ... */;

  const value_type& operator[](key_type) const;
        value_type& operator[](key_type);

  static value_type& operator[](key_type); // since C++23
};

Note that operator[] can accept multiple parameters since C++23.

Member Access Operators

struct X {
  using value_type = /* ... */;

  const value_type& operator*() const; // indirection operator
        value_type& operator*();

  const value_type* operator->() const; // arrow operator
        value_type* operator->();
};

Pointer-to-Member Operator

struct X {
  using member_type = /* ... */;
  using member_pointer_type = /* ... */;
  
  const member_type& operator->*(member_pointer_type) const;
        member_type& operator->*(member_pointer_type);
};

Address-of Operator

struct X {
  using address_type = /* ... */;

  address_type operator&() const; // address-of operator
};

Logical Operators

struct X {
  friend X operator&&(const X&, const X&); // logical AND
  friend X operator||(const X&, const X&); // logical OR
  friend X operator!(const X&);            // logical NOT
};

Note that these don't return bool because they only make sense if X is already a logical type that similar to bool.

User-defined Conversions

struct X {
  using type = /* ... */;

  operator type() const;          // arbitrary implicit conversion

  explicit operator bool() const; // explicit/contextual conversion to bool

  template <typename T>
    requires /* ... */            // optionally constrained
  explicit operator T() const;    // conversion function template
};

Coroutine Await

struct X {
  using awaiter = /* ... */;

  awaiter operator co_await() const;
};

Comma Operator

struct X {
  using pair_type = /* ... */;

  // often a template to support combination of arbitrary types
  friend pair_type operator,(const X&, const X&);
};

Allocation Functions

struct X {
  // class-specific allocation functions
  void* operator new(std::size_t);
  void* operator new[](std::size_t);
  void* operator new(std::size_t, std::align_val_t); // C++17
  void* operator new[](std::size_t, std::align_val_t); // C++17

  // class-specific placement allocation functions
  void* operator new(std::size_t, user-defined-args...);
  void* operator new[](std::size_t, user-defined-args...);
  void* operator new(std::size_t, std::align_val_t, user-defined-args...); // C++17
  void* operator new[](std::size_t, std::align_val_t, user-defined-args...); // C++17

  // class-specific usual deallocation functions
  void operator delete(void*);
  void operator delete[](void*);
  void operator delete(void*, std::align_val_t); // C++17
  void operator delete[](void*, std::align_val_t); // C++17
  void operator delete(void*, std::size_t);
  void operator delete[](void*, std::size_t);
  void operator delete(void*, std::size_t, std::align_val_t); // C++17
  void operator delete[](void*, std::size_t, std::align_val_t); // C++17

  // class-specific placement deallocation functions
  void operator delete(void*, user-defined-args...);
  void operator delete(void*, user-defined-args...);

  // class-specific usual destroying deallocation functions
  void operator delete(X*, std::destroying_delete_t); // C++20
  void operator delete(X*, std::destroying_delete_t, std::align_val_t); // C++20
  void operator delete(X*, std::destroying_delete_t, std::size_t); // C++20
  void operator delete(X*, std::destroying_delete_t, std::size_t, std::align_val_t); // C++20
};

// non-class specific replaceable allocation functions ...

void* operator new(std::size_t);
void* operator delete(void*);
// ...
黎夕旧梦 2025-01-22 23:14:27

简单来说,我将提到一些要点,这些要点是我在过去一周学习 Python 和 C++、OOP 以及其他东西时得出的,所以内容如下:

  1. The <运算符的 href="https://en.wiktionary.org/wiki/arity#Noun" rel="nofollow noreferrer">元数 不能进一步修改!

  2. 重载运算符只能有一个默认参数,而函数调用运算符则不能。

  3. 只有内置运算符可以重载,其余的都不能!

更多信息,您可以参考运算符重载规则,它将您重定向到 GeeksforGeeks 提供的文档。

Making it short and simple, I'll be referring to some points, which I had come over the past week as I was learning Python and C++, OOP and other things, so it goes as follows:

  1. The arity of the operator can not be modified further than to what it is!

  2. Overloaded operators can only have one default argument which the function call operator rest it cannot.

  3. Only built-in operators can be overloaded, and the rest can't!

For more information, you can refer to Rules for operator overloading, which redirects you to the documentation provided by GeeksforGeeks.

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