提问人:sbi 提问时间:12/12/2010 最后编辑:Jan Schultkesbi 更新时间:11/23/2023 访问量:1011349
算子重载的基本规则和习语是什么?
What are the basic rules and idioms for operator overloading?
问:
注意:答案是按特定顺序给出的,但由于许多用户根据投票而不是给出的时间对答案进行排序,因此以下是答案最有意义的顺序的索引:
- C++ 中运算符重载的一般语法
- C++ 中运算符重载的三个基本规则
- 会员与非会员之间的决定
- 重载的常见运算符
- 赋值运算符
- 流插入和提取
- 函数调用运算符
- 逻辑运算符
- 算术运算符
- 下标运算符
- 类似指针类型的运算符
- 比较运算符,包括 C++20 三元比较
- 转换运算符
- 重载 new 和 delete
- 规范函数签名摘要
(注意:这是Stack Overflow的C++常见问题解答的条目。如果您想批评以这种形式提供常见问题解答的想法,那么在开始这一切的元上发帖就是这样做的地方。该问题的答案在C++聊天室中受到监控,FAQ的想法首先从那里开始,因此您的答案很可能会被提出该想法的人阅读。
答:
C++中运算符重载的三个基本规则
当涉及到 C++ 中的运算符重载时,您应该遵循三个基本规则。与所有这些规则一样,确实有例外。有时人们偏离了它们,结果不是糟糕的代码,但这种积极的偏差很少,而且相距甚远。至少,在我见过的 100 个这样的偏差中,有 99 个是不合理的。但是,它也可能是 999 分中的 1000 分。所以你最好遵守以下规则。
每当运算符的含义不明显和无可争议时,就不应过载。 相反,请为函数提供一个精心选择的名称。
基本上,超载运算符的首要规则,其核心是:不要这样做。这可能看起来很奇怪,因为有很多关于运算符重载的知识,所以很多文章、书籍章节和其他文本都涉及所有这些。但是,尽管有这些看似显而易见的证据,但令人惊讶的是,只有极少数情况下操作员超载是合适的。原因是,实际上很难理解运算符应用背后的语义,除非运算符在应用域中的使用是众所周知的且无可争议的。与普遍的看法相反,情况并非如此。始终坚持运算符众所周知的语义。
C++ 对重载运算符的语义没有限制。编译器会很乐意接受实现二进制运算符的代码,以从其右操作数中减去。但是,这种运算符的用户永远不会怀疑表达式要从 中减去。当然,这假设运算符在应用域中的语义是无可争议的。+
a + b
a
b
始终提供一组相关操作中的所有操作。
运算符彼此相关,并与其他操作相关。如果您的类型支持 ,用户也希望能够调用 。如果它支持 prefix increment ,它们也有望工作。如果他们能检查是否 ,他们肯定会期望也能够检查是否 。如果他们可以复制构造你的类型,他们希望赋值也能正常工作。a + b
a += b
++a
a++
a < b
a > b
继续查看会员与非会员之间的决定。
评论
boost::spirit
+
&
<<
>>
operator==
a
b
a
b
float
C++ 中运算符重载的一般语法
不能更改 C++ 中内置类型的运算符的含义,只能重载用户定义类型1 的运算符。也就是说,至少有一个操作数必须是用户定义的类型。与其他重载函数一样,运算符只能为一组特定的参数重载一次。
并非所有运算符都可以在 C++ 中重载。不能重载的运算符包括: 和 C++ 中唯一的三元运算符,.
::
sizeof
typeid
.*
?:
在 C++ 中可以重载的运算符包括:
类别 | 运营商 | Arity 和位置 |
---|---|---|
算术 | + - * / % 和+= -= *= /= %= |
二进制中缀 |
+ - |
一元前缀 | |
++ -- |
一元前缀和后缀 | |
位 | & | ^ << >> 和&= |= ^= <<= >>= |
二进制中缀 |
~ |
一元前缀 | |
比较 | == != < > <= >= <=> |
二进制中缀 |
逻辑 | || && |
二进制中缀 |
! |
一元前缀 | |
分配函数 | new new[] delete delete[] |
一元前缀 |
用户定义的转换 | T |
元 |
分配 | = |
二进制中缀 |
会员访问 | -> ->* |
二进制中缀 |
间接/地址 | * & |
一元前缀 |
函数调用 | () |
N-ary 后缀 |
下标 | [] |
N-ary2 后缀 |
协程等待 | co_await |
一元前缀 |
逗点 | , |
二进制中缀 |
但是,您可以重载所有这些并不意味着您应该这样做。请参阅运算符重载的基本规则。
在 C++ 中,运算符以具有特殊名称的函数的形式重载。与其他函数一样,重载运算符通常可以作为其左操作数类型的成员函数或非成员函数实现。您是否可以自由选择或必须使用其中任何一个取决于几个标准。3 应用于对象 x 的一元运算符 4 被调用为 或 。应用于对象和 的二进制中缀运算符称为 或 。5@
operator@(x)
x.operator@()
@
x
y
operator@(x,y)
x.operator@(y)
作为非成员函数实现的运算符有时是其操作数类型的友元。
1 术语“用户定义”可能略有误导。C++ 区分了内置类型和用户定义类型。前者属于例如 int、char 和 double;后者属于所有结构、类、联合和枚举类型,包括来自标准库的类型,即使它们本身不是由用户定义的。
2 下标运算符曾经是二进制的,而不是 N-ary,直到 C++23。
3 本常见问题解答的后面部分将对此进行介绍。
4 @
在 C++ 中不是一个有效的运算符,这就是我使用它作为占位符的原因。
5 C++ 中唯一的三元运算符不能重载,唯一的 n 元运算符必须始终作为成员函数实现。
继续阅读 C++ 中运算符重载的三个基本规则。
评论
重载的常用运算符
重载运算符的大部分工作都是样板代码。这并不奇怪,因为运算符只是句法糖。它们的实际工作可以由普通函数完成(并且经常被转发给普通函数)。但重要的是,你要正确地编写这个样板代码。如果失败,要么操作员的代码无法编译,要么用户的代码无法编译,要么用户代码的行为令人惊讶。
赋值运算符
关于分配,有很多话要说。然而,其中大部分已经在 GMan 著名的 Copy-And-Swap FAQ 中说过了,所以我在这里跳过大部分内容,只列出完美的赋值运算符供参考:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
流插入和提取
免責聲明 |
---|
对于重载和按位移位运算符,请跳到二进制算术运算符部分。<< >> |
按位移位运算符 和 虽然在硬件接口中仍用于它们从 C 继承的位操作函数,但在大多数应用程序中,作为重载流输入和输出运算符已变得更加普遍。<<
>>
流运算符是最常见的重载运算符之一,是二进制中缀运算符,其语法未指定任何限制,即它们应该是成员还是非成员。 但是,它们的左操作数是来自标准库的流,您不能向这1 添加成员函数,因此您需要为自己的类型实现这些运算符作为非成员函数2。 两者的规范形式如下:
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 另请注意,后缀变体会做更多的工作,因此使用起来比前缀变体效率低。这是通常首选前缀增量而不是后缀增量的一个很好的理由。虽然编译器通常可以优化内置类型的后缀增量的额外工作,但它们可能无法对用户定义的类型执行相同的操作(这可能看起来像列表迭代器一样无辜)。一旦你习惯了做 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+=
返回每个引用的结果,同时返回其结果的副本。当然,返回引用通常比返回副本更有效,但在 的情况下,没有办法绕过复制。当你写 时,你希望结果是一个新值,这就是为什么必须返回一个新值。1operator+
operator+
a + b
operator+
另请注意,可以通过按值传递而不是通过引用来稍微缩短。
但是,这将泄露实现细节,使函数签名不对称,并阻止命名返回值优化,其中对象与返回的对象相同。operator+
lhs
result
有时,在 方面实现是不切实际的,例如矩阵乘法。
在这种情况下,您还可以委托给:@
@=
@=
@
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 +=
b 比 a + 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 版本。
对于运算符,如果 是 (or or ) 类型,则递归调用另一个运算符,直到 an 返回非类类型的值。->
value_type
class
struct
union
operator->()
operator->()
一元地址运算符不应重载。
对于看这个问题。它很少使用,因此很少超载。事实上,即使是迭代器也不会使它过载。operator->*()
继续转换运算符。
评论
operator->()
其实是极其诡异的。它不需要返回 -- 事实上,它可以返回另一个类类型,前提是该类类型有一个运算符 >(),
然后随后会调用它。这种对 s 的递归调用一直持续到出现返回类型。疯狂!:)value_type*
operator->()
value_type*
operatorX
operatorX=
operator*=
operator*
*
*=
*=
*this
T* const
const T&
T const *
const_iterator
reference_type operator*() const; pointer_type operator->() const
会员与非会员之间的决定
类别 | 运营商 | 决定 |
---|---|---|
必需的成员函数 | [] , , , , ...() = -> |
成员函数(由 C++ 标准强制要求) |
指向成员访问的指针 | ->* |
成员函数 |
元 | ++ , , , , ...- * new |
成员函数,枚举除外 |
复合分配 | += , , , ...|= *= |
成员函数,枚举除外 |
其他运营商 | + , , , , ...== <=> / |
首选非会员 |
二进制运算符(赋值)、(数组订阅)、(成员访问)以及 n-ary(函数调用)运算符必须始终作为成员函数实现,因为语言的语法要求它们这样做。=
[]
->
()
其他运算符既可以作为成员实现,也可以作为非成员实现。但是,其中一些通常必须作为非成员函数实现,因为它们的左操作数不能由您修改。其中最突出的是输入和输出运算符 和 ,其左操作数是标准库中的流类,您无法更改。<<
>>
对于必须选择将其实现为成员函数或非成员函数的所有运算符,请使用以下经验法则来决定:
- 如果它是一元运算符,则将其实现为成员函数。
- 如果二进制运算符对两个操作数一视同仁(它使它们保持不变),则将此运算符实现为非成员函数。
- 如果二进制运算符没有平等地对待其两个操作数(通常它会更改其左操作数),那么如果它必须访问操作数的私有部分,则使其成为其左操作数类型的成员函数可能很有用。
当然,与所有经验法则一样,也有例外。如果您有类型
enum Month {Jan, Feb, ..., Nov, Dec}
并且您想重载它的递增和递减运算符,您不能将其作为成员函数来执行,因为在 C++ 中,枚举类型不能具有成员函数。因此,您必须将其重载为免费函数。对于嵌套在类中的类模板,当作为成员函数在类定义中内联时,模板的编写和读取要容易得多。但这些确实是罕见的例外。operator<()
(但是,如果您做了一个例外,请不要忘记操作数的 -ness 问题,对于成员函数来说,这个问题将成为隐式参数。如果作为非成员函数的运算符将其最左边的参数作为引用,则与成员函数相同的运算符需要在末尾有一个才能进行引用。const
this
const
const
*this
const
继续执行重载的常用运算符。
评论
operator+=()
operator +=
append
append
append(string, start, end)
+=
start = 0
end = string.size
operator +=
重载和运算符new
delete
注意:这只涉及重载 new
和 delete
的语法,而不涉及此类重载运算符的实现。我认为重载 new
和 delete
的语义值得有自己的常见问题解答,在运算符重载的主题中,我永远无法做到公正。
基本
在 C++ 中,当您编写一个新表达式时,在计算此表达式时会发生以下两种情况:首先调用运算符 new
以获取原始内存,然后调用相应的构造函数 将此原始内存转换为有效对象。同样,删除对象时,首先调用其析构函数,然后将内存返回到 。
C++ 允许您调整这两个操作:内存管理和在分配的内存上构造/销毁对象。后者是通过为类编写构造函数和析构函数来完成的。微调内存管理是通过编写自己的 和 来完成的。new T(arg)
T
operator delete
operator new
operator delete
运算符重载的第一条基本规则 - 不要这样做 - 特别适用于重载和 .这些运算符过载的几乎唯一原因是性能问题和内存限制,在许多情况下,其他操作(如对所用算法的更改)将提供比尝试调整内存管理高得多的成本/增益比。new
delete
C++ 标准库附带了一组预定义的运算符。其中最重要的有这些:new
delete
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
operator delete
operator new
operator delete
new
delete
放置new
C++ 允许 new 和 delete 运算符采用其他参数。
所谓的 placement new 允许您在某个地址创建一个对象,该对象被传递给:
class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{
X* p = new(buffer) X(/*...*/);
// ...
p->~X(); // call destructor
}
为此,标准库附带了适当的 new 和 delete 运算符重载:
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();
请注意,在上面给出的放置 new 的示例代码中,除非 X 的构造函数抛出异常,否则永远不会被调用。operator delete
您还可以重载 and with other arguments。与 placement new 的附加参数一样,这些参数也列在关键字 后面的括号内。仅仅出于历史原因,这种变体通常也被称为放置新,即使它们的论点不是为了将对象放置在特定地址。new
delete
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,只需将标准库的预定义运算符替换为我们自己的运算符即可。然而,这很少需要这样做。
评论
nothrow
[array] [{ placement | nothrow }] { new | delete }
nothrow new
nothrow
new(buffer) X(/*...*/)
实际上,什么保证了正确对齐?buffer
X
转换运算符(也称为用户定义的转换)
在 C++ 中,可以创建转换运算符,这些运算符允许编译器在类型和其他定义的类型之间进行转换。有两种类型的转换运算符:隐式和显式运算符。
隐式转换运算符(C++98/C++03 和 C++11)
隐式转换运算符允许编译器将用户定义类型的值隐式转换(如 和之间的转换)转换为其他类型。int
long
下面是一个带有隐式转换运算符的简单类:
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()
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_;
};
请注意 .现在,当您尝试从隐式转换运算符执行意外代码时,会出现编译器错误:explicit
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*’
要调用显式强制转换运算符,您必须使用 、 C 样式强制转换或构造函数样式强制转换 ( 即 ).static_cast
T(value)
但是,有一个例外:允许编译器隐式转换为 .此外,编译器在转换为 后不允许执行其他隐式转换(编译器一次允许执行 2 次隐式转换,但最多只能执行 1 次用户定义的转换)。bool
bool
由于编译器不会强制转换“past”,因此显式转换运算符现在不需要 Safe Bool 习惯用语。例如,C++11 之前的智能指针使用 Safe Bool 习惯用来防止转换为整型类型。在 C++11 中,智能指针改用显式运算符,因为不允许编译器在将类型显式转换为 bool 后隐式转换为整型类型。bool
为什么用于将对象流式传输到 std::cout
或文件的 operator<<
函数不能成为成员函数?
假设您有:
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;
由于 被重载为 的成员函数,运算符的 LHS 必须是一个对象。这意味着,您将需要使用:operator<<
Foo
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;
这是非常直观的。
评论
<<
std::cout
<<
为了简短起见,我将提到一些观点,这些观点是我在过去一周学习Python和C++,OOP和其他东西时提出的,所以它如下:
操作员的灵活性不能再修改到它是什么了!
重载运算符只能有一个默认参数,函数调用运算符 rest 不能。
只有内置运算符可以重载,其余的不能!
有关更多信息,您可以参考运算符重载的规则,这会将您重定向到 GeeksforGeeks 提供的文档。
比较运算符,包括三向比较(C++20)
有相等比较和 ,
和关系比较 , , , .
C++20 还引入了三向比较算子。==
!=
<
>
<=
>=
<=>
算子 | 含义及注意事项 (旧) | 含义和注释 (C++20) |
---|---|---|
x == y |
true if 和 are equal 满足 EqualityComparable (使用 x y std::unordered_map ) |
(x <=> y) == 0 (通常直接实现,除非 委托给三方) 满足 std::equality_comparable = default |
x != y |
!(x == y) |
!(x == y) |
x < y |
如果低于 满足 LessThanComparable (由 、 等使用,但需要严格的弱排序,则为 true, 但需要严格的弱排序 x y 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 |
不适用 | 三向比较 又名。“宇宙飞船操作员” 满足 标准::three_way_comparable |
指引
- 比较运算符不应是成员函数。1)
- 如果定义,也定义(除非用 C++20 重写)。
==
!=
- 如果定义 、 定义 、 和 too。
<
>
<=
>=
- (C++20)比起定义每个关系运算符,更喜欢定义。
<=>
- (C++20)首选默认运算符,而不是手动实现。
- 相等和关系比较应匹配,这意味着
应等同于 2)x == y
!(x < y) && !(y < x)
- 不要用 来定义,即使你可以 3)
==
<
1) 否则,隐式转换将是不对称的,并且 ==
应将相同类型的隐式转换应用于两端。
2) 这种等价不适用于浮点
数,但适用于 int
和其他强有序类型。
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 中,所有这些通常都可以是 noexcept
和 constexpr
。
如果我们有一个部分有序的成员(例如)。
在这种情况下,必须以不同的方式编写。<
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}
2 < 0
字典比较是实现严格弱排序的简单方法,对于像这样的容器和像这样的算法来说,这是必需的。简而言之,严格的弱排序应该像整数的运算符一样,只是允许某些整数是等价的(例如,对于所有偶数整数,都是假的)。std::set
std::sort
<
x < y
如果等价于 ,则可以采用更简单的方法: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::lexicographical_compare
。
有些人使用宏或奇怪的重复模板模式 (CRTP) 来保存委派、、和 的样板,或者模仿 C++20 的三向比较。!=
>
>=
<=
也可以使用 std::rel_ops
(在 C++20 中已弃用)来委托 、、 和 to 和 for 某些范围内的所有类型。!=
>
<=
>=
<
==
默认比较 (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
表达式重写 (C++20)
在 C++20 中,如果没有直接实现比较,编译器也会尝试使用重写候选项。
多亏了这一点,即使没有默认(这将实现所有运算符),我们只需要实现 和 ,所有其他比较都会根据这两个重写。<=>
==
<=>
算子 | 潜在的重写 |
---|---|
x == y |
y == x |
x != y |
!(x == y) 或者,如果相等比较返回!(y == x) bool |
x < y |
(x <=> y) < 0 或者如果比较结果与零相当0 < (y <=> x) |
x > y |
(x <=> y) > 0 或者如果......0 > (y <=> x) |
x <= y |
(x <=> y) <= 0 或者如果......0 <= (y <=> x) |
x >= y |
(x <=> y) >= 0 或者如果......0 >= (y <=> x) |
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 { /* ... */ }
};
注意:constexpr
和 noexcept
是可选的,但几乎总是可以应用于比较运算符。
三向比较运算符 (C++20)
注:俗称“宇宙飞船操作员”。另请参阅 spaceship-operator。
背后的基本思想是,结果告诉我们是小于、大于、等价还是无序。
这类似于 C 中的函数。x <=> y
x
y
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 |
less , ,equal = equivalent greater |
std::weak_ordering |
用户自定义1) | less , ,equivalent greater |
std::partial_ordering |
float |
less , , ,equivalent greater unordered |
std::strong_ordering
s 可以转换为 ,而 可以转换为 。
这些类别的值与(例如)相当,这与上述函数具有相似的含义。
但是,对于所有比较,返回 false。std::weak_ordering
std::partial_ordering
(x <=> y) == 0
compare
std::partial_ordering::unordered
1) 没有 x <=>y
导致 std::weak_ordering
的基本类型。强排序和弱排序在实践中是可以互换的;请参阅 std::strong_ordering 和 std::weak_ordering 的实际含义。
手动实现三向比较
三向比较通常是默认的,但可以手动实现,例如:
#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;
};
如果 的所有成员都不是同一类型,那么我们可以显式指定类别(在返回类型中),也可以使用 std::common_comparison_category
获取它:S
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_three_way
。
规范函数签名摘要
许多运算符重载几乎可以返回任何内容。例如,没有什么能阻止您返回 。
但是,这些签名中只有少数是规范的,这意味着您通常会以这种方式编写它们,并且此类运算符可以显式默认为 .void
operator==
= 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->();
};
Pointer-to-Member 运算符
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
};
协程等待
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*);
// ...
评论