sbi*_*sbi 2074 c++ operator-overloading operators c++-faq
注意:答案是按照特定的顺序给出的,但由于许多用户根据投票而不是给出的时间对答案进行排序,因此这里是答案的索引,它们是最有意义的顺序:
(注意:这是Stack Overflow的C++常见问题解答的一个条目.如果你想批评在这种形式下提供常见问题解答的想法,那么发布所有这些的元数据的发布将是这样做的地方.这个问题在C++聊天室中受到监控,其中FAQ的想法一开始就出现了,所以你的答案很可能被那些提出想法的人阅读.)
sbi*_*sbi 1007
超载运营商的大部分工作都是锅炉板代码.这并不奇怪,因为操作符只是语法糖,它们的实际工作可以通过(通常转发到)普通函数来完成.但重要的是你要正确使用这种锅炉板代码.如果您失败了,您的操作员代码将无法编译,或者您的用户代码将无法编译,或者您的用户代码将出现令人惊讶的行为.
关于任务有很多话要说.但是,大部分内容已经在GMan着名的Copy-And-Swap常见问题解答中说过了,所以我将在这里跳过大部分内容,仅列出完美的赋值运算符以供参考:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Run Code Online (Sandbox Code Playgroud)
位移操作符提升<<和>>,虽然在硬件接口用于它们选自C继承的位操作功能仍在使用,已成为如在大多数应用中重载流的输入和输出操作员更加普遍.有关作为位操作运算符的指导重载,请参阅下面的二进制算术运算符部分.要在对象与iostream一起使用时实现自己的自定义格式和解析逻辑,请继续.
流运算符(最常见的是重载运算符)是二进制中缀运算符,其语法对它们应该是成员还是非成员没有限制.由于他们改变了他们的左参数(他们改变了流的状态),根据经验法则,他们应该被实现为左操作数类型的成员.但是,它们的左操作数是来自标准库的流,虽然标准库定义的大多数流输出和输入操作符确实被定义为流类的成员,但当您为自己的类型实现输出和输入操作时,无法更改标准库的流类型.这就是为什么你需要为你自己的类型实现这些运算符作为非成员函数.这两者的规范形式是:
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;
}
Run Code Online (Sandbox Code Playgroud)
实现时operator>>,只有在读取本身成功时才需要手动设置流的状态,但结果不是预期的结果.
用于创建函数对象的函数调用运算符(也称为函子)必须定义为成员函数,因此它始终具有this成员函数的隐式参数.除此之外,它可以重载以获取任意数量的附加参数,包括零.
这是一个语法示例:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Run Code Online (Sandbox Code Playgroud)
用法:
foo f;
int a = f("hello");
Run Code Online (Sandbox Code Playgroud)
在整个C++标准库中,始终复制函数对象.因此,您自己的功能对象应该便宜复制.如果函数对象绝对需要使用复制成本高昂的数据,最好将该数据存储在其他地方并让函数对象引用它.
根据经验法则,二进制中缀比较运算符应该实现为非成员函数1.一元前缀否定!应该(根据相同的规则)实现为成员函数.(但重载它通常不是一个好主意.)
标准库的算法(例如std::sort())和类型(例如std::map)将始终只operator<存在.但是,您的类型的用户也希望所有其他运算符都存在,因此如果您定义operator<,请确保遵循运算符重载的第三个基本规则,并定义所有其他布尔比较运算符.实现它们的规范方法是:
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
Run Code Online (Sandbox Code Playgroud)
这里要注意的重要一点是,这些操作符中只有两个实际上做了任何事情,其他操作符只是将它们的参数转发给这两个中的任何一个来完成实际工作.
重载剩余的二进制布尔运算符(||,&&)的语法遵循比较运算符的规则.然而,这是非常不可能的,你会发现这些合理的使用案例2.
1 与所有经验法则一样,有时可能有理由打破这一个.如果是的话,不要忘记二进制比较运算,这对于成员函数将是左侧操作数*this,必须是const也.因此,作为成员函数实现的比较运算符必须具有以下签名:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
Run Code Online (Sandbox Code Playgroud)
(注意const最后.)
2 应该注意的是内置版本||和&&使用快捷方式的语义.虽然用户定义的(因为它们是方法调用的语法糖)不使用快捷语义.用户希望这些运算符具有快捷语义,并且它们的代码可能依赖于它,因此强烈建议永远不要定义它们.
一元递增和递减运算符有前缀和后缀两种风格.为了告诉另一个,后缀变体采用额外的伪int参数.如果重载增量或减量,请确保始终实现前缀和后缀版本.这是增量的规范实现,减量遵循相同的规则:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Run Code Online (Sandbox Code Playgroud)
请注意,后缀变体是根据前缀实现的.另请注意,postfix会额外复制.2
重载一元减号和加号不是很常见,可能最好避免.如果需要,它们可能应该作为成员函数重载.
2 另请注意,后缀变体功能更多,因此使用效率低于前缀变量.这是一个很好的理由,通常更喜欢前缀增量而不是后缀增量.虽然编译器通常可以优化内置类型的后缀增量的额外工作,但是它们可能无法对用户定义的类型执行相同的操作(这可能是像列表迭代器那样无辜地看起来的东西).一旦你习惯了i++,很难记住,++i当i不是内置类型时(更改类型时你必须更改代码),所以最好养成一个习惯使用前缀增量,除非明确需要postfix.
对于二进制算术运算符,不要忘记遵守第三个基本规则运算符重载:如果你提供+,也提供+=,如果你提供-,不要省略-=等.据说Andrew Koenig是第一个观察到复合赋值的人运算符可以用作非复合对应物的基础.也就是说,运营商+来讲是执行+=,-在以下方面实现-=等.
根据我们的经验法则,+它的同伴应该是非成员,而他们的复合作业对应物(+=等),改变他们的左派论点,应该是成员.下面是示例性代码+=和+,其他二元算术运算符应该以同样的方式来实现:
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
Run Code Online (Sandbox Code Playgroud)
operator+=返回每个引用的结果,同时operator+返回其结果的副本.当然,返回引用通常比返回副本更有效,但在这种情况下operator+,无法复制.在编写时a + b,您希望结果是一个新值,这就是为什么operator+必须返回一个新值.3
另请注意,operator+它的左操作数是复制而不是const引用.其原因与给出operator=每份副本的理由相同.
位操作运算符~ & | ^ << >>应该以与算术运算符相同的方式实现.但是,(除了重载<<和>>输出和输入之外)很少有合理的用例来重载这些.
3 同样,从中可以得出的教训a += b是,一般来说,a + b如果可能的话,效率高于且应该优先考虑.
数组下标运算符是二元运算符,必须作为类成员实现.它用于容器类型,允许通过键访问其数据元素.提供这些的规范形式是这样的:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
Run Code Online (Sandbox Code Playgroud)
除非您不希望类的用户能够更改返回的数据元素operator[](在这种情况下您可以省略非const变量),否则应始终提供运算符的两种变体.
如果已知value_type引用内置类型,则运算符的const变量应返回副本而不是const引用.
要定义自己的迭代器或智能指针,必须重载一元前缀解除引用运算符*和二进制中缀指针成员访问运算符->:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Run Code Online (Sandbox Code Playgroud)
请注意,这些也几乎总是需要const和非const版本.对于->操作者,如果value_type是的class(或struct或union)类型,另一个operator->()被递归调用,直到operator->()返回非类类型的值.
一元地址运算符永远不应该重载.
对于operator->*()看这个问题.它很少使用,因此很少超载.事实上,即使是迭代器也不会使它超载.
继续转换操作员
sbi*_*sbi 481
当谈到C++中的运算符重载时,您应该遵循三个基本规则.与所有此类规则一样,确实存在例外情况.有时人们偏离了他们,结果并不是糟糕的代码,但这种积极的偏差很少.至少,我所看到的100个这样的偏差中有99个是没有道理的.但是,它可能只有1000中的999.所以你最好坚持以下规则.
只要操作员的意义不明显且无可争议,就不应该超载. 相反,提供具有良好选择名称的功能.
基本上,重载运营商的第一个也是最重要的规则是:不要这样做.这可能看起来很奇怪,因为有很多关于运算符重载的知识,因此很多文章,书籍章节和其他文本都涉及到这一切.但尽管有这些看似明显的证据,但只有极少数情况下运营商超载是合适的.原因是实际上很难理解运算符应用背后的语义,除非在应用程序域中使用运算符是众所周知且无可争议的.与普遍看法相反,情况并非如此.
始终坚持运营商众所周知的语义.
C++对重载运算符的语义没有限制.您的编译器将很乐意接受实现二元+运算符的代码从其右操作数中减去.然而,这样的运营商的用户将不会怀疑表达a + b减去a从b.当然,这假设应用程序域中的运算符的语义是无可争议的.
始终提供一系列相关操作.
运营商彼此之间以及与其他运营相关.如果您的类型支持a + b,用户也可以打电话a += b.如果它支持前缀增量++a,它们也a++可以工作.如果他们可以检查是否a < b,他们肯定也希望能够检查是否a > b.如果他们可以复制构造您的类型,他们希望分配也可以工作.
继续执行会员与非会员之间的决定.
sbi*_*sbi 258
您无法在C++中更改内置类型的运算符的含义,只能为用户定义的类型1重载运算符.也就是说,至少一个操作数必须是用户定义的类型.与其他重载函数一样,运算符只能为一组参数重载一次.
并非所有运算符都可以在C++中重载.在无法重载的运算符中有:. :: sizeof typeid .*和C++中唯一的三元运算符,?:
可以在C++中重载的运算符包括:
+ - * / %和+= -= *= /= %=(所有二进制中缀); + -(一元前缀); ++ --(一元前缀和后缀)& | ^ << >>和&= |= ^= <<= >>=(所有二进制中缀); ~(一元前缀)== != < > <= >= || &&所有二进制中缀); !(一元前缀)new new[] delete delete[]= [] -> ->* , 所有二进制中缀); * &(所有一元前缀)()(函数调用,n-ary中缀)但是,你可以超载所有这些并不意味着你应该这样做.请参阅运算符重载的基本规则.
在C++中,运算符以具有特殊名称的函数形式重载.为具有其他功能,重载操作符可以通常被实现为一个其左操作数的类型的成员函数或作为非成员函数.您是否可以自由选择或限制使用其中任何一个取决于几个标准.2应用于对象x的一元运算符@3可以作为operator@(x)或作为调用x.operator@().二元缀运算符@,施加到对象x和y被称为或者作为operator@(x,y)或作为x.operator@(y).4
实现为非成员函数的运算符有时是其操作数类型的朋友.
1 "用户定义"一词可能略有误导.C++区分内置类型和用户定义类型.前者属于例如int,char和double; 后者属于所有struct,class,union和enum类型,包括来自标准库的类型,即使它们不是由用户定义的.
2 这是覆盖在后面的部分这个常见问题.
3 这@不是C++中的有效运算符,这就是我将其用作占位符的原因.
4 C++中唯一的三元运算符不能重载,唯一的n-ary运算符必须始终作为成员函数实现.
继续使用C++中的运算符重载的三个基本规则.
sbi*_*sbi 240
二元运算符=(赋值),[](数组预订),->(成员访问)以及n-ary ()(函数调用)运算符必须始终作为成员函数实现,因为语言的语法要求它们.
其他运营商可以作为成员或非成员实施.但是,其中一些通常必须作为非成员函数实现,因为它们的左操作数不能被您修改.最突出的这些是输入和输出操作符<<和>>,它的左操作数是从你不能改变标准库流类.
对于您必须选择将它们实现为成员函数或非成员函数的所有运算符,请使用以下经验法则来决定:
当然,与所有经验法则一样,也有例外.如果你有类型
enum Month {Jan, Feb, ..., Nov, Dec}
Run Code Online (Sandbox Code Playgroud)
并且你想为它重载递增和递减运算符,你不能将它作为成员函数来执行,因为在C++中,枚举类型不能具有成员函数.所以你必须将它作为一个自由函数重载.和operator<()嵌套类模板中的类模板是写起来更简单,当作为类定义的成员函数内联方式完成阅读.但这些确实是罕见的例外.
(但是,如果你做了一个例外,不要忘记const操作数的-ness 问题,对于成员函数,成为隐式this参数.如果作为非成员函数的操作符将其最左边的参数作为const参考中,相同的操作者作为成员函数需要具有const在端部,使*this一const参考).
继续向Common运算符重载.
JKo*_*Kor 159
在C++中,您可以创建转换运算符,这些运算符允许编译器在您的类型和其他已定义类型之间进行转换.有两种类型的转换运算符,隐式和显式运算符.
的隐式转换运算符允许编译器隐式转换(像之间的转换int和long用户定义的类型的值),以一些其它类型的.
以下是一个带隐式转换运算符的简单类:
class my_string {
public:
operator const char*() const {return data_;} // This is the conversion operator
private:
const char* data_;
};
Run Code Online (Sandbox Code Playgroud)
隐式转换运算符(如单参数构造函数)是用户定义的转换.在尝试匹配对重载函数的调用时,编译器将授予一个用户定义的转换.
void f(const char*);
my_string str;
f(str); // same as f( str.operator const char*() )
Run Code Online (Sandbox Code Playgroud)
起初这看起来非常有用,但问题在于隐式转换甚至在不期望的情况下启动.在下面的代码中,void f(const char*)将调用因为my_string()不是左值,所以第一个不匹配:
void f(my_string&);
void f(const char*);
f(my_string());
Run Code Online (Sandbox Code Playgroud)
初学者很容易弄错,甚至经验丰富的C++程序员有时会感到惊讶,因为编译器选择了他们没有怀疑的过载.显式转换运算符可以减轻这些问题.
与隐式转换运算符不同,显式转换运算符在您不期望它们时将永远不会启动.以下是具有显式转换运算符的简单类:
class my_string {
public:
explicit operator const char*() const {return data_;}
private:
const char* data_;
};
Run Code Online (Sandbox Code Playgroud)
请注意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*’
要调用显式强制转换操作符,必须使用static_castC样式强制转换或构造函数样式转换(即T(value)).
但是,有一个例外:允许编译器隐式转换为bool.此外,在转换为bool编译器之后,不允许编译器执行另一个隐式转换(允许编译器一次执行2次隐式转换,但最多只能执行1次用户定义的转换).
因为编译器不会bool转换"过去" ,所以显式转换运算符现在不再需要Safe Bool习语.例如,C++ 11之前的智能指针使用Safe Bool习惯用法来防止转换为整数类型.在C++ 11中,智能指针使用显式运算符,因为在将类型显式转换为bool之后,不允许编译器隐式转换为整数类型.
继续加载new和delete.
sbi*_*sbi 145
new和delete注意:这只涉及重载的语法,new而delete不是这些重载运算符的实现.我认为重载的语义new和delete配得上自己的常见问题解答,在运算符重载的主题内我永远无法做到公道.
在C++中,当你写一个新的表达像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();
Run Code Online (Sandbox Code Playgroud)
前两个为对象分配/释放内存,后两个为对象数组.如果您提供自己的版本,它们不会超载,而是替换标准库中的那些.
如果你重载operator new,你应该总是重载匹配operator delete,即使你从不打算调用它.其原因在于,如果一个构造一个新的表达式的计算过程中抛出,运行时系统将内存返回operator delete匹配operator new的是被称为分配给创建的对象的内存.如果你不提供匹配operator delete,调用默认值,这几乎总是错误的.
如果你超载new和delete,你应该考虑超载数组变量了.
newC++允许new和delete运算符采用其他参数.
所谓的placement new允许你在某个地址创建一个对象,该地址传递给:
class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{
X* p = new(buffer) X(/*...*/);
// ...
p->~X(); // call destructor
}
Run Code Online (Sandbox Code Playgroud)
标准库附带了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();
Run Code Online (Sandbox Code Playgroud)
请注意,在上面给出的placement new示例代码中,operator delete永远不会调用,除非X的构造函数抛出异常.
您也可以重载new和delete使用其他参数.与放置new的附加参数一样,这些参数也列在关键字后面的括号内new.仅仅由于历史原因,这些变体通常也称为放置新的,即使它们的参数不是用于将对象放置在特定地址.
最常见的是,您需要微调内存管理,因为测量已经显示特定类或一组相关类的实例经常被创建和销毁,并且运行时系统的默认内存管理已经过调整一般表现,在这种特定情况下效率低下.要改进这一点,您可以为特定类重载new和delete:
class my_class {
public:
// ...
void* operator new();
void operator delete(void*,std::size_t);
void* operator new[](size_t);
void operator delete[](void*,std::size_t);
// ...
};
Run Code Online (Sandbox Code Playgroud)
因此重载,new和delete的行为类似于静态成员函数.对于对象my_class,std::size_t参数将永远是sizeof(my_class).但是,也会调用这些运算符来动态分配派生类的对象,在这种情况下,它可能比这更大.
要重载全局new和delete,只需用我们自己的标准库替换标准库的预定义运算符.但是,这很少需要完成.
R S*_*ahu 42
为什么不能operator<<将流对象std::cout作为成员函数传递给文件?
假设你有:
struct Foo
{
int a;
double b;
std::ostream& operator<<(std::ostream& out) const
{
return out << a << " " << b;
}
};
Run Code Online (Sandbox Code Playgroud)
鉴于此,你不能使用:
Foo f = {10, 20.0};
std::cout << f;
Run Code Online (Sandbox Code Playgroud)
由于operator<<作为成员函数重载Foo,运算符的LHS必须是Foo对象.这意味着,您将被要求使用:
Foo f = {10, 20.0};
f << std::cout
Run Code Online (Sandbox Code Playgroud)
这是非常不直观的.
如果将其定义为非成员函数,
struct Foo
{
int a;
double b;
};
std::ostream& operator<<(std::ostream& out, Foo const& f)
{
return out << f.a << " " << f.b;
}
Run Code Online (Sandbox Code Playgroud)
您将能够使用:
Foo f = {10, 20.0};
std::cout << f;
Run Code Online (Sandbox Code Playgroud)
这非常直观.
有相等比较 ==和!=,以及关系比较 <, >, <=, >=。C++20还引入了三路比较运算符<=>。
| 操作员 | 含义和注释(旧) | 含义和注释 (C++20) |
|---|---|---|
x == y |
true 如果x和y相等满足EqualityComparable (由 所使用 std::unordered_map) |
(x <=> y) == 0(通常直接实现,不 委托给三路除非 = default)满足 std::equality_comparable |
x != y |
!(x == y) |
!(x == y) |
x < y |
true 如果x低于y满足LessThanComparable (由、等 使用,但需要严格的弱排序) std::setstd::sort |
(x <=> y) < 0当包裹在函子中时可能会满足(例如) std::strict_weak_orderingstd::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 部分。 |
所有运算符通常都实现为非成员函数,可能作为隐藏的友元(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); }
};
Run Code Online (Sandbox Code Playgroud)
注意:在 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; }
Run Code Online (Sandbox Code Playgroud)
operator<的实现operator<并不那么简单,因为正确的字典比较不能简单地比较每个成员一次。
{1, 2} < {3, 0}应该是真的,即使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;
}
Run Code Online (Sandbox Code Playgroud)
对于多个成员,可以使用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);
}
};
Run Code Online (Sandbox Code Playgroud)
用于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;
};
Run Code Online (Sandbox Code Playgroud)
注意:默认的比较运算符必须是friend类的,实现这一点的最简单方法是将它们定义为类内的默认值。这使他们成为“隐藏的朋友”。
或者,我们可以默认单独的比较运算符。如果我们想定义相等比较或仅定义关系比较,这很有用:
friend bool operator==(const S&, const S&) = default; // inside S
Run Code Online (Sandbox Code Playgroud)
在 C++20 中,如果没有直接实现比较,编译器也会尝试使用重写候选者。因此,即使不是<=>默认的(这将实现所有运算符),我们也只需实现==and <=>,并且所有其他比较都根据这两者重写。
| 操作员 | 潜在的重写 |
|---|---|
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 { /* ... */ }
};
Run Code Online (Sandbox Code Playgroud)
注意:constexpr和noexcept是可选的,但几乎总是可以应用于比较运算符。
注:俗称“宇宙飞船操作员”。另请参见spaceship-operator。
背后的基本思想x <=> y是,结果告诉我们是否x低于、大于、等于或无序y。strcmp这与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
}
Run Code Online (Sandbox Code Playgroud)
该运算符的结果不是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_orderings 可以转换为std::weak_ordering, 可以转换为std::partial_ordering. 这些类别的值与 (eg ) 相当(x <=> y) == 0,这与compare上面的函数具有相似的含义。但是,std::partial_ordering::unordered所有比较都返回 false。
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;
};
Run Code Online (Sandbox Code Playgroud)
如果 的所有成员S不是同一类型,那么我们可以显式指定类别(在返回类型中),或者可以通过以下方式获取它std::common_comparison_category:
std::common_comparison_category_t<decltype(l.x <=> l.x), /* ... */>
Run Code Online (Sandbox Code Playgroud)
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);
}
};
Run Code Online (Sandbox Code Playgroud)
用于std::lexicographical_compare_three_way数组成员。
| 归档时间: |
|
| 查看次数: |
884712 次 |
| 最近记录: |