注意:答案是按照特定的顺序给出的,但由于许多用户是根据投票结果而不是给出答案的时间来排序的,所以下面是答案的索引,按答案最有意义的顺序排列:
(注意:这是Stack Overflow的C++FAQ的一个条目。如果你想要批评在这个表单中提供FAQ的想法,那么开始这一切的meta上的帖子将是这样做的地方。这个问题的答案在C++聊天室中被监视,FAQ的想法最初是在那里开始的,所以你的答案很可能被提出这个想法的人阅读。)
重载操作员中的大部分工作都是锅炉板代码。 这也就不足为奇了,因为运算符仅仅是语法上的糖,它们的实际工作可以由普通函数来完成(并且通常被转发给普通函数)。 但重要的是,你要正确地得到这个锅炉板代码。 如果您失败,要么您的操作员代码无法编译,要么您的用户代码无法编译,要么您的用户代码的行为将令人惊讶。
关于任务有很多要说的。 不过大部分在GMAN著名的复制交换FAQ中已经说过了,所以这里略过大部分,只列出完美的赋值运算符供参考:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
位移位运算符和
>>
虽然仍然用于硬件接口,用于它们从C继承的位操作函数,但在大多数应用程序中作为重载流输入和输出运算符已变得更加普遍。 有关作为位操作运算符重载的指导,请参阅下面的二进制算术运算符一节。 要在对象与iostreams一起使用时实现您自己的自定义格式和解析逻辑,请继续。
流运算符是最常见的重载运算符之一,是二进制中缀运算符,其语法对它们是成员还是非成员没有任何限制。 由于它们改变了它们的左参数(它们改变了流的状态),根据经验规则,它们应该被实现为它们的左操作数类型的成员。 然而,它们的左操作数是来自标准库的流,并且尽管标准库定义的大多数流输出和输入运算符确实被定义为流类的成员,但当您为自己的类型实现输出和输入操作时,您不能更改标准库的流类型。 这就是为什么您需要为自己的类型实现这些运算符作为非成员函数的原因。 两者的规范形式如下:
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>>
时,只有当读取本身成功时才需要手动设置流的状态,但结果并不是预期的。
函数调用运算符,用于创建函数对象,也称为函子,必须定义为成员函数,因此它始终具有成员函数的隐式this
参数。 除此之外,它可以被重载以接受任意数量的附加参数,包括零。
下面是一个语法示例:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
用法:
foo f;
int a = f("hello");
在整个C++标准库中,函数对象总是被复制的。 因此,您自己的函数对象复制起来应该很便宜。 如果function对象绝对需要使用复制成本较高的数据,最好将该数据存储在其他地方,并让function对象引用该数据。
根据经验规则,二进制中缀比较运算符应该实现为非成员函数1。 一元前缀否定!
应该(根据相同的规则)作为成员函数实现。 (但过载通常不是个好主意。)
标准库的算法(例如std::sort()
)和类型(例如std::map
)始终只期望存在运算符<;
。 但是,您的类型的用户将期望所有其他运算符也存在,因此如果您定义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);}
这里需要注意的重要一点是,这些操作符中只有两个实际上做任何事情,其他操作符只是将它们的参数转发给这两个操作符中的任何一个来做实际的工作。
重载其余二进制布尔运算符(,
&&
)的语法遵循比较运算符的规则。 但是,您很不可能找到这些2的合理用例。
1就像所有的经验法则一样,有时也会有理由违反这一法则。 如果是这样,不要忘记二进制比较运算符的左边操作数(对于成员函数来说是*this
)也需要是const
。 因此实现为成员函数的比较运算符必须具有以下签名:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(注意末尾的常量
。)
2应该注意,和
&&
的内置版本使用快捷语义。 而用户定义的函数(因为它们是方法调用的语法糖)不使用快捷语义。 用户希望这些运算符具有快捷语义,而它们的代码可能依赖于快捷语义,因此强烈建议永远不要定义它们。
一元递增运算符和递减运算符有前缀和后缀两种形式。 为了区别它们,后缀变体采用一个额外的虚拟int参数。 如果您重载增量或减量,请确保始终同时实现前缀和后缀版本。 下面是增量的规范实现,递减遵循同样的规则:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
注意,后缀变体是根据前缀实现的。 还要注意,后缀执行额外的复制。2
重载一元减号和加号并不常见,最好避免。 如果需要,可能应该将它们作为成员函数重载。
2还要注意,后缀变体比前缀变体做更多的工作,因此使用效率较低。 这是一个很好的理由,通常倾向于前缀增量而不是后缀增量。 尽管编译器通常可以优化内置类型的后缀增量的额外工作,但它们可能无法对用户定义类型(可能是看起来像列表迭代器一样无辜的东西)进行相同的操作。 一旦您习惯了执行i++
,当i
不是内置类型时,就很难记住执行++i
(另外,在更改类型时您必须更改代码),因此最好养成总是使用前缀增量的习惯,除非显式需要后缀。
对于二进制算术运算符,不要忘记遵守运算符重载的第三个基本规则:如果你提供+
,也要提供+=
,如果你提供-
,不要省略-=
等等。据说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;
}
operator+=
返回其每个引用的结果,而operator+
返回其结果的副本。 当然,返回引用通常比返回副本更有效,但在operator+
的情况下,无法绕过复制。 当您编写a+b
时,您希望结果是一个新值,这就是为什么operator+
必须返回一个新值的原因。3还要注意,operator+
通过复制而不是通过常量引用获取其左操作数。 其原因与operator=
每次获取参数的原因相同。
位操作运算符~
^
应以与算术运算符相同的方式实现。 但是(除了重载
<
和
>>
用于输出和输入),重载这些的合理用例非常少。
3同样,从这方面可以得到的教训是,a+=b
通常比a+b
更有效,如果可能,应该首选a+=b
数组下标运算符是二进制运算符,必须作为类成员实现。 它用于类似容器的类型,这些类型允许通过键访问它们的数据元素。 提供这些信息的规范形式如下:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
除非您不希望您的类的用户能够更改operator[]
返回的数据元素(在这种情况下,您可以省略非常量变量),否则您应该始终提供运算符的两个变量。
如果已知value_type引用内置类型,则运算符的const变量应该更好地返回副本而不是const引用:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
要定义自己的迭代器或智能指针,必须重载一元前缀取消引用运算符*
和二进制中缀指针成员访问运算符->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
请注意,它们几乎总是同时需要常量和非常量版本。 对于->
运算符,如果value_type
是class
(或struct
或union
)类型,则递归调用另一个运算符->()
,直到一个运算符->()
返回非类类型的值。
不应重载一元addres-of运算符。
对于运算符->*()
,请参见此问题。 它很少使用,因此很少过载。 事实上,即使迭代器也不会重载它。
继续转换运算符
当谈到C++中的运算符重载时,有三条基本规则您应该遵循。 与所有这些规则一样,确实也有例外。 有时人们偏离了它们,结果并不是糟糕的代码,但是这种积极的偏离是很少的。 最低限度,在我所看到的100个这样的偏离中,有99个是不合理的。 然而,也可能是1000个中的999个。 所以你最好坚持以下规则。
>
每当运算符的含义不是明显明确和无可争议时,就不应该重载它。 实际上,重载运算符的首要规则是:Don't do it(不要这样做)。
基本上,重载运算符的首要规则是:Don't do it(不要这样做)。 这可能看起来很奇怪,因为有很多关于运算符重载的知识要知道,所以很多文章,书籍章节和其他文本都在讨论这一切。 但是尽管有这看似明显的证据,只有少得惊人的情况下操作符重载是合适的。 原因在于,实际上很难理解运算符应用程序背后的语义,除非运算符在应用程序域中的使用是众所周知的并且无可争议的。 与普遍的看法相反,情况几乎从来都不是这样。
始终坚持运算符的众所周知的语义。
C++对重载运算符的语义没有任何限制。 您的编译器将欣然接受实现二进制+
运算符的代码,以便从它的右操作数中减去。 但是,这样的运算符的用户绝不会怀疑表达式a+b
从b
中减去a
。 当然,这假定运算符在应用程序域中的语义是无可争议的。
始终提供一组相关操作中的所有操作。
运算符彼此相关,并与其他操作相关。 如果您的类型支持a+b
,则用户也希望能够调用a+=b
。 如果它支持前缀增量++a
,则他们会期望a++
也能工作。 如果他们能检查a<; B
,他们肯定也希望能够检查A>; b
。 如果他们能够复制-构造您的类型,他们希望赋值也能工作。
继续进行成员和非成员之间的决定。
您不能在C++中为内置类型更改运算符的含义,运算符只能为用户定义类型1重载。 也就是说,至少有一个操作数必须是用户定义的类型。 与其他重载函数一样,对于某个参数集,运算符只能重载一次。
并非所有运算符都可以在C++中重载。 不能重载的运算符包括:.
::
sizeof
typeid
.*
和C++中唯一的三元运算符?:
在C++中可以重载的运算符包括:
+
-
*
/
%
和+=
*=
/=
%=
(均为二进制中缀); +
-
(一元前缀); ++
--
(一元前缀和后缀)~
(一元前缀)==
!=
(全部为二进制中缀); !
(一元前缀)
新建
新建[]
删除
删除[]
=
[]
->
->*
,
(均为二进制中缀); *
&
(全一元前缀)()
(函数调用,n元中缀)然而,你可以超载所有这些并不意味着你应该这样做。 参见运算符重载的基本规则。
在C++中,运算符是以带有特殊名称的函数形式重载的。 与其他函数一样,重载运算符通常可以实现为其左操作数类型的成员函数,也可以实现为非成员函数。 您是可以自由选择还是必须使用其中之一取决于几个条件。应用于对象x的2一元运算符@
3可以作为operator@(x)
或作为x.operator@()
调用。 应用于对象x
和y
的二进制中缀运算符@
称为运算符@(x,y)
或x.运算符@(y)
。4
作为非成员函数实现的运算符有时是其操作数类型的友元。
1术语“用户定义”可能略有误导。 C++对内置类型和用户定义类型进行了区分。 前者属于例如int,char和double; 后者属于所有struct,class,union和enum类型,包括来自标准库的类型,即使它们本身不是由用户定义的。
2本常见问题解答的后面部分将介绍这一点。
3@
在C++中不是有效的运算符,这就是我使用它作为占位符的原因。
4C++中唯一的三元运算符不能重载,唯一的n元运算符必须始终作为成员函数实现。
继续讲C++中运算符重载的三个基本规则。