要重载的常用运算符
重载运算符的大部分工作都是样板代码。这不足为奇,因为运算符只是语法糖。他们的实际工作可以通过(并且通常被转发到)普通函数来完成。但重要的是您要正确获取此样板代码。如果失败,要么你的操作员的代码将无法编译,你的用户的代码将无法编译,或者你的用户的代码将表现得令人惊讶。
赋值运算符
关于任务,有很多话要说。不过大部分内容已经在GMan 著名的复制和交换常见问题解答 https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom,所以我在这里跳过大部分内容,只列出完美的赋值运算符以供参考:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
流插入和提取
Disclaimer |
For overloading << and >> as bitwise shift operators, skip to the section Binary Arithmetic Operators. |
按位移位运算符<<
and >>
尽管仍然在硬件接口中使用它们从 C 继承的位操作函数,但在大多数应用程序中作为重载流输入和输出运算符已变得更加普遍。
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;
}
实施时operator>>
,只有当读取本身成功,但结果不是预期的时候才需要手动设置流的状态。
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.
函数调用运算符
函数调用运算符,用于创建函数对象,也称为functors https://en.wikipedia.org/wiki/Functor_(functional_programming),必须定义为member函数,因此它总是具有隐含的this
成员函数的参数。除此之外,它可以被重载以接受任意数量的附加参数,包括零。
下面是语法示例:
struct X {
// Overloaded call operator
int operator()(const std::string& y) {
return /* ... */;
}
};
Usage:
X f;
int a = f("hello");
在整个 C++ 标准库中,函数对象总是被复制。因此,您自己的函数对象的复制成本应该很低。如果函数对象绝对需要使用复制成本高昂的数据,那么最好将该数据存储在其他地方并让函数对象引用它。
比较运算符
This section has been moved elsewhere |
See this FAQ answer https://stackoverflow.com/questions/4421706/operator-overloading-in-c/77055231#77055231 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 C++20 https://en.wikipedia.org/wiki/C%2B%2B20:
#include <compare>
struct X {
// defines ==, !=, <, >, <=, >=, <=>
friend auto operator<=>(const X&, const X&) = default;
};
如果您无法执行此操作,请继续查看链接的答案。
逻辑运算符
一元前缀否定!
应该作为成员函数来实现。超载通常不是一个好主意,因为它非常罕见且令人惊讶。
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.
算术运算符
一元算术运算符
一元递增和递减运算符有前缀和后缀两种形式。为了区分其中一个,后缀变体需要一个额外的虚拟 int 参数。如果重载增量或减量,请确保始终实现前缀和后缀版本。
这是增量的规范实现,减量遵循相同的规则:
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
重载一元减号和加号并不常见,最好避免。如果需要,它们可能应该作为成员函数重载。
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.
二元算术运算符
对于二元算术运算符,不要忘记遵守运算符重载的第三个基本规则:如果您提供+
,还提供+=
,如果您提供-
, 不要省略-=
, etc. 安德鲁·科尼格 https://en.wikipedia.org/wiki/Andrew_Koenig_(programmer)据说是第一个观察到复合赋值运算符可以用作非复合赋值运算符的基础。也就是说,运营商+
是根据以下方面实施的+=
, -
是根据以下方面实施的-=
, etc.
根据我们的经验法则,+
及其同伴应该是非成员,而其复合赋值对应者(+=
等),改变他们的左参数,应该是一个成员。这是示例代码+=
and +
;其他二元算术运算符应以相同的方式实现:
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
另请注意operator+
可以通过传递稍微缩短lhs
按值,而不是按引用。
然而,这会泄漏实现细节,使函数签名不对称,并且会阻止命名返回值优化 where result
与返回的对象是同一对象。
有时,实施起来并不切实际@
按照@=
,例如矩阵乘法。
在这种情况下,您还可以委托@=
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
}
};
位操作运算符~
&
|
^
<<
>>
应该以与算术运算符相同的方式实现。但是,(除了超载<<
and >>
对于输出和输入),很少有合理的用例来重载它们。
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.
下标运算符
下标运算符是二元运算符,必须作为类成员实现。它用于类似容器的类型,允许通过键访问其数据元素。
提供这些的规范形式是这样的:
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 和非常量版本。
为了->
运算符,如果value_type
is of class
(or struct
or union
)类型,另一个operator->()
被递归调用,直到operator->()
返回非类类型的值。
一元取址运算符永远不应该被重载。
For operator->*()
see 这个问题 https://stackoverflow.com/q/8777845/140719。它很少被使用,因此很少过载。事实上,即使迭代器也不会重载它。
继续转换运算符 https://stackoverflow.com/questions/4421706/operator-overloading/16615725#16615725.