- C++中运算符重载的一般语法
- C++中运算符重载的三个基本规则
- 成员函数和非成员函数的选择
- 常见的运算符重载
- 赋值运算符
- 流插入和提取运算符
- 函数调用运算符
- 逻辑运算符
- 算术运算符
- 下标运算符
- 指针类似类型的运算符
- 比较运算符,包括C++20的三路比较
- 转换运算符
- 重载new和delete运算符
- 典型函数签名总结
重载运算符的大部分工作都是样板代码。这并不奇怪,因为运算符只是语法糖。它们的实际工作可以由普通函数完成(通常也是转发给普通函数)。但是,重要的是要正确处理这些样板代码。如果处理不当,要么你的运算符代码无法编译,要么用户的代码无法编译,要么用户的代码会出现意外行为。
关于赋值运算符有很多要说的。然而,大部分内容已经在GMan's famous Copy-And-Swap FAQ中提到了,所以我在这里只列出了完美的赋值运算符作为参考:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
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>>
时,只有在读取本身成功但结果不符合预期时,才需要手动设置流的状态。<<
重载是作为成员函数实现的,而另一些是作为自由函数实现的。只有与区域设置相关的函数是成员函数,例如operator<<(long)
。this
参数。除此之外,它可以重载以接受任意数量的附加参数,包括零个。struct X {
// Overloaded call operator
int operator()(const std::string& y) {
return /* ... */;
}
};
使用方法:
X f;
int a = f("hello");
<=>
设置为默认值,来重载所有比较运算符,这在 C++20 中是可行的。#include <compare>
struct X {
// defines ==, !=, <, >, <=, >=, <=>
friend auto operator<=>(const X&, const X&) = default;
};
!
应该被实现为一个成员函数。通常不建议重载它,因为它很少被使用,而且容易令人惊讶。struct X {
X operator!() const { return /* ... */; }
};
X operator&&(const X& lhs, const X& rhs) { return /* ... */; }
X operator||(const X& lhs, const X& rhs) { return /* ... */; }
struct X {
X& operator++()
{
// Do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
+
,也要提供+=
;如果提供了-
,不要省略-=
,依此类推。据说安德鲁·科尼格是第一个观察到复合赋值运算符可以作为其非复合对应物的基础的人。也就是说,运算符+
是基于+=
实现的,-
是基于-=
实现的,依此类推。+
及其相关运算符应该是非成员函数,而改变左操作数的复合赋值对应物(+=
等)应该是成员函数。以下是+=
和+
的示例代码;其他二进制算术运算符应该以相同的方式实现: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+
返回其结果的副本。当然,返回引用通常比返回副本更高效,但在 operator+
的情况下,无法避免复制。当你写 a + b
时,你期望结果是一个新值,这就是为什么 operator+
必须返回一个新值。1
还要注意,通过按值传递 lhs
可以稍微缩短 operator+
的长度。
然而,这将泄漏实现细节,使函数签名不对称,并且将阻止命名返回值优化,其中 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
}
};
struct X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
operator[]
返回的数据元素(在这种情况下,您可以省略非const变体),否则应始终提供运算符的两个变体。
为了定义自己的迭代器或智能指针,您必须重载一元前缀解引用运算符*
和二元中缀指针成员访问运算符->
:
struct my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
operator->()
实际上非常奇怪。它不需要返回一个 value_type*
—— 事实上,它可以返回另一个类类型,**只要该类类型有一个 operator->()
**,那么这个 operator->()
将随后被调用。这种递归调用 operator->()
会一直进行,直到出现 value_type*
返回类型。疯狂的操作! :) - j_random_hacker*=
来定义*
,但这样做会很笨拙,因为*=
的第一个操作是创建一个新对象,即计算结果。然后,在for-ijk循环之后,我们会将这个临时对象与*this
交换。也就是说:1.复制,2.运算符 *
,3.交换。 - Luc Hermitteconst value_type& operator*() const;
- 这就像在取消引用时返回T* const
以返回const T&
一样,这并不是真实情况。或者换句话说:const指针并不意味着const指向物。事实上,模仿T const *
并不简单,这也是标准库中整个const_iterator
的原因。结论:签名应该是reference_type operator*() const; pointer_type operator->() const
。 - Arne Mertzoperator<=>()
最终确定下来(在C++20中),比较运算符部分需要进行更新。 - Toby Speight在C++中进行运算符重载时,应该遵循三个基本规则。像所有规则一样,确实存在例外情况。有时人们偏离这些规则也可以得到不错的代码,但这种积极的偏离是少数。至少我看到的100个偏离中,99个都没有正当理由。然而,实际上可能是1000个偏离中的999个。所以最好遵循以下规则。
只有当运算符的含义明显清晰时,它才应该被重载。相反,提供一个命名合适的函数。
基本上,运算符重载的第一条规则就是:不要这样做。这可能看起来很奇怪,因为关于运算符重载有很多知识需要掌握,所以很多文章、书籍章节和其他文本都在处理所有这些内容。但尽管有这些表面上明显的证据,实际上只有极少数情况下适用于运算符重载。原因是除非在应用领域中广泛使用的运算符的用法已经被明确了解并被广泛接受,否则实际上很难理解运算符应用背后的语义。与普遍看法相反,这几乎从未发生过。
始终坚持这个运算符的良好语义。
C++对重载运算符的语义没有任何限制。您的编译器将高兴地接受将二元+
运算符实现为从其右操作数中减去的代码。但是,这样的运算符的使用者永远不会想到表达式a + b
将从b
中减去a
。当然,这假设运算符在应用领域中的语义是已知且无争议的。
始终提供一组相关操作的所有输出。
操作符彼此相关,也与其他操作相关。如果您的类型支持a + b
,用户也会期望能够调用a += b
。如果它支持前缀递增++a
,他们也会期望a++
也能正常工作。如果他们可以检查a < b
,他们肯定也会希望能够检查a > b
。如果他们可以复制构造您的类型,他们期望赋值也能正常工作。
继续阅读成员和非成员之间的决策。
boost::spirit
哈哈。 - Billy ONeal+
来进行字符串连接是一种违反规范的行为。不过,由于现在已经成为了一种行业惯例,因此看起来很自然。尽管我还记得在90年代看到一个家庭制作的字符串类使用二进制 &
作为此目的的符号(参考BASIC的惯例)。但是,是的,将其放入标准库基本上已经定下了这个惯例。同样的情况也适用于滥用 <<
和 >>
进行输入输出。为什么左移会成为显而易见的输出操作呢?因为我们在看到第一个“Hello, world!”应用程序时就学习了它,没有其他原因。 - sbioperator==
唯一绝对明显且无争议的事情是它应该是一个等价关系(即你不应该使用非信号NaN)。容器上有许多有用的等价关系。相等的含义是什么?“a
等于b
”的意思是a
和b
具有相同的数学值。一个(非NaN)float
的数学值的概念是清晰的,但是一个容器的数学值可以有许多不同的(类型递归)有用的定义。最强的相等定义是“它们是相同的对象”,但这是无用的。 - curiousguy类别 | 运算符 | 决策 |
---|---|---|
强制成员函数 | [] , () , = , -> , ... |
成员函数(由C++标准要求) |
指向成员的访问 | ->* |
成员函数 |
一元运算符 | ++ , - , * , new , ... |
成员函数,除了枚举类型 |
复合赋值运算符 | += , |= , *= , ... |
成员函数,除了枚举类型 |
其他运算符 | + , == , <=> , / , ... |
优先使用非成员函数 |
当然,像所有经验法则一样,也有例外情况。如果你有一个类型
enum Month {Jan, Feb, ..., Nov, Dec}
operator<()
作为成员函数内联在类定义中,写起来更容易理解。但这些情况确实很少见。const
性质的问题,对于成员函数来说,它变成了隐式的this
参数。如果作为非成员函数的运算符将其最左边的参数作为const
引用,那么作为成员函数的同一个运算符需要在末尾加上const
,以使*this
成为一个const
引用。)
operator+=()
不作为成员函数的想法。它需要改变左操作数,因此必须深入其内部实现。如果不将其设置为成员函数,你会得到什么好处呢? - sbioperator +=
和append
方法。append
方法更加完整,因为你可以将参数中从索引 i 到 n - 1 的子字符串追加进去:append(string, start, end)
。让+=
调用append
并将start=0
和end=string.size
似乎是有道理的。那时,append
可以是成员方法,但是operator +=
不需要是成员,将其变为非成员会减少操作字符串内部的代码量,所以这是一件好事…… ^_^ … - paercebal在C++中,你不能改变内置类型的运算符的含义,运算符只能被重载用于用户自定义类型1。也就是说,至少有一个操作数必须是用户自定义类型。与其他重载函数一样,运算符只能被重载一次,针对一组特定的参数。
并非所有的运算符都可以在C++中被重载。不能被重载的运算符包括:.
::
sizeof
typeid
.*
,以及C++中唯一的三元运算符?:
可以在C++中重载的运算符包括以下几种:
类别 | 运算符 | 元数和位置 |
---|---|---|
算术 | + - * / % 和 += -= *= /= %= |
二元中缀 |
+ - |
一元前缀 | |
++ -- |
一元前缀和后缀 | |
位运算 | & | ^ << >> 和 &= |= ^= <<= >>= |
二元中缀 |
~ |
一元前缀 | |
比较 | == != < > <= >= <=> |
二元中缀 |
逻辑 | || && |
二元中缀 |
! |
一元前缀 | |
分配函数 | new new[] delete delete[] |
一元前缀 |
用户定义的转换 | T |
一元 |
赋值 | = |
二元中缀 |
成员访问 | -> ->* |
二元中缀 |
间接/取地址 | * & |
一元前缀 |
函数调用 | () |
N元后缀 |
下标 | [] |
N元2后缀 |
协程等待 | co_await |
一元前缀 |
逗号 | , |
二元中缀 |
1 “用户定义”这个术语可能有点误导。C++区分内置类型和用户定义类型。前者包括int、char和double;后者包括所有的struct、class、union和enum类型,包括标准库中的类型,尽管它们并非由用户定义。
2 直到C++23,下标运算符一直是二元的,而不是N元的。
3 这在本FAQ的后面部分有详细介绍。
4 C++中的@
不是有效的运算符,所以我将其用作占位符。
5 C++中唯一的三元运算符不能重载,而唯一的N元运算符必须始终作为成员函数实现。
.*
。 - celticminstrel:)
- sbioperator+()
作为成员函数进行重载,但是给它了一个自由函数的签名。请参见这里。 - sbi在C++中,您可以创建转换运算符,这些运算符允许编译器在您的类型和其他定义的类型之间进行转换。有两种类型的转换运算符,即隐式和显式转换运算符。
隐式转换运算符允许编译器将用户定义类型的值(例如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()
不是lvalue,所以第一个不匹配:void f(my_string&);
void f(const char*);
f(my_string());
class my_string {
public:
explicit operator const char*() const {return data_;}
private:
const char* data_;
};
explicit
。现在当您尝试执行从隐式转换运算符的意外代码时,会出现编译器错误:
prog.cpp: 在函数‘int main()’中: prog.cpp:15:18: 错误:没有找到与‘f(my_string)’匹配的函数 prog.cpp:15:18: 注意:候选函数有: prog.cpp:11:10: 注意:void f(my_string&) prog.cpp:11:10: 注意: 无法将参数1从‘my_string’转换为‘my_string&’ prog.cpp:12:10: 注意:void f(const char*) prog.cpp:12:10: 注意: 无法将参数1从‘my_string’转换为‘const char*’要调用显式转换运算符,您必须使用
static_cast
、C样式转换或构造函数样式转换(即T(value)
)。bool
。此外,在将类型转换为bool
后,编译器不允许进行其他隐式转换(编译器可以同时进行2个隐式转换,但最多只能进行1个用户定义的转换)。bool
,所以显式转换运算符现在消除了安全布尔值习惯用法的需要。例如,在C++11之前,智能指针使用安全布尔值习惯用法来防止转换为整数类型。在C++11中,智能指针使用显式运算符代替,因为编译器不允许在明确将类型转换为bool后隐式转换为整数类型。new
和delete
。new
和delete
运算符注意: 这只涉及重载new
和delete
的语法,而不是其实现。我认为在操作符重载的主题中,重载new
和delete
值得拥有它们自己的常见问题解答,我永远无法做到完美。
在C++中,当你编写new表达式如new T(arg)
时,这个表达式被评估时会发生两件事情:首先,调用operator new
来获取原始内存,然后调用T
的适当构造函数将这个原始内存转换为有效对象。同样,当你删除一个对象时,首先会调用它的析构函数,然后将内存返回给operator delete
。
C++允许你调整这两个操作:内存管理和分配内存时的对象构造/销毁。后者是通过为一个类编写构造函数和析构函数来完成的。调整内存管理则需要编写自己的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();
class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{
X* p = new(buffer) X(/*...*/);
// ...
p->~X(); // call destructor
}
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
。new
和delete
。与放置新的附加参数一样,在关键字new
后的括号中列出这些参数。仅出于历史原因,即使它们的参数不是为了在特定地址放置对象,这些变体通常也被称为放置新。new
和delete
最常见的情况是,您需要微调内存管理,因为测量表明,经常创建和销毁特定类或一组相关类的实例,并且运行时系统的默认内存管理对于这种特定情况效率不高。为了改进这一点,您可以为特定类重载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)
。但是,这些运算符也用于动态分配的派生类对象,此时参数可能大于sizeof(my_class)
。
要重载全局new和delete,只需用我们自己的运算符替换标准库的预定义运算符即可。然而,这很少需要做。
为什么无法将用于将流式对象插入到 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;
由于 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
一起使用,但是如果重载<<
与同一类一起使用呢?在这种情况下,它可以成为成员函数,对吗? - KansaiRobot操作符 | 含义和注释(旧版) | 含义和注释(C++20) |
---|---|---|
x == y |
如果x 和y 相等,则为真满足EqualityComparable (被 std::unordered_map 使用) |
(x <=> y) == 0 (通常直接实现,不委托给三路比较,除非 = default )满足 std::equality_comparable |
x != y |
!(x == y) |
!(x == y) |
x < y |
如果x 小于y ,则为真满足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 |
不适用 | 三路比较 又称为“太空船操作符” 满足 std::three_way_comparable |
==
,也要定义!=
(除非在C++20中被重写)。<
,也要定义>
、<=
和>=
。<=>
而不是定义每个关系运算符。x == y
应该等价于!(x < y) && !(y < x)
2)<
来定义==
,即使你可以3)1) 否则,隐式转换将是不对称的,而且预期==
会对两边应用相同类型的隐式转换。
2) 这种等价性不适用于float
,但适用于int
和其他强有序类型。
3) 这是为了提高可读性、正确性和性能。
免责声明 |
---|
如果您正在使用C++20,本节中的实现已经过时。 除非您对历史视角感兴趣,否则请直接跳到C++20部分。 |
所有运算符通常被实现为非成员函数,可能作为hidden friends(函数在类内定义的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); }
};
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中已弃用)将!=
、>
、<=
和>=
委托给某个作用域中所有类型的<
和==
。
大部分比较运算符只是比较类的每个成员。 如果是这样的话,实现就是纯样板代码,我们可以让编译器完成所有工作:
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
操作符 | 可能的重写 |
---|---|
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 { /* ... */ }
};
// 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 |
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;
};
S
的所有成员都不是相同类型,那么我们可以在返回类型中明确指定类别,或者我们可以使用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_three_way
来比较数组成员。许多运算符重载可以返回几乎任何东西。例如,在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;
};
有关何时以及如何设置/实现比较,请参阅this answer获取更多信息。
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
};
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
};
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*);
// ...
运算符的元数不能进一步修改!
重载运算符只能有一个默认参数,函数调用运算符则不能。
只有内置运算符可以被重载,其他运算符不能。
更多信息,请参考以下链接,该链接将引导您访问GeekforGeeks提供的文档。
operator&
。 - Red.Waveoperator&()
然后期望生成的类型能够与标准库一起使用的人应该被迫实现执行此操作的std lib。换句话说,如果您认为您有一个重载此运算符的应用程序,我很想听听它。(不过不要指望会有掌声。) - sbi