运算符重载的基本规则和习语是什么?

2450
注意:答案是按照特定顺序给出的,但由于许多用户根据投票而不是给出时间来排序答案,因此这里是答案的索引,按照它们最有意义的顺序排列: (注意:这是一个Stack Overflow的C++ FAQ的条目。如果你想批评以这种形式提供FAQ的想法,那么这个meta帖子将是你发表意见的地方。对于那个问题的回答在C++聊天室中进行监控,FAQ的想法最初就是在那里提出的,所以你的回答很可能会被那些提出这个想法的人读到。)

75
如果我们要继续使用C++-FAQ标签,那么条目应该按照以下格式进行编排。 - John Dibling
2
我为德国C++社区撰写了一系列关于运算符重载的短文:第1部分:C++中的运算符重载涵盖了所有运算符的语义、典型用法和特殊性。它与您在此处的答案有些重叠,但仍有一些额外信息。第2部分和第3部分是使用Boost.Operators的教程。您是否希望我将它们翻译并添加为答案? - Arne Mertz
2
哦,以下是程序相关内容的翻译: 基础知识常用实践 - Arne Mertz
1
缺少取地址运算符 operator& - Red.Wave
1
@Red.Wave:实际上,在常见运算符答案的结尾,即使在自己的段落中也有一句话,但它说“不要这样做”。我认为是Dinkumware的Pete Becker(该公司制作了后来被Microsoft收购的std lib)曾经说过,那些重载operator&()然后期望生成的类型能够与标准库一起使用的人应该被迫实现执行此操作的std lib。换句话说,如果您认为您有一个重载此运算符的应用程序,我很想听听它。(不过不要指望会有掌声。) - sbi
显示剩余4条评论
10个回答

1201

常见的重载运算符

重载运算符的大部分工作都是样板代码。这并不奇怪,因为运算符只是语法糖。它们的实际工作可以由普通函数完成(通常也是转发给普通函数)。但是,重要的是要正确处理这些样板代码。如果处理不当,要么你的运算符代码无法编译,要么用户的代码无法编译,要么用户的代码会出现意外行为。

赋值运算符

关于赋值运算符有很多要说的。然而,大部分内容已经在GMan's famous 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>>时,只有在读取本身成功但结果不符合预期时,才需要手动设置流的状态。
注意,标准库的一些<<重载是作为成员函数实现的,而另一些是作为自由函数实现的。只有与区域设置相关的函数是成员函数,例如operator<<(long)
根据经验法则,插入/提取运算符应该是成员函数,因为它们修改左操作数。然而,在这里我们不能遵循经验法则。
函数调用运算符用于创建函数对象,也称为functors,必须定义为成员函数,因此它始终具有成员函数的隐式this参数。除此之外,它可以重载以接受任意数量的附加参数,包括零个。
以下是语法示例:
struct X {
    // Overloaded call operator
    int operator()(const std::string& y) {
        return /* ... */;
    }
};

使用方法:

X f;
int a = f("hello");

在整个C++标准库中,函数对象总是被复制。因此,你自己的函数对象应该是廉价的可复制的。如果一个函数对象绝对需要使用昂贵的数据进行复制,最好将该数据存储在其他地方,并让函数对象引用它。
比较运算符
此部分已移至其他位置
有关重载二元中缀运算符==、!=、<、>、<=和>=,以及C++20中的<=>三路比较(也称为“太空船运算符”),请参阅this FAQ answer。关于比较运算符有很多要说的,超出了本答案的范围。
在最简单的情况下,你可以通过将 <=> 设置为默认值,来重载所有比较运算符,这在 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 /* ... */; }

应该注意到,内置版本的`||`和`&&`使用了快捷语义。而用户定义的版本(因为它们是方法调用的语法糖)不使用快捷语义。用户期望这些运算符具有快捷语义,并且他们的代码可能依赖于此,因此强烈建议永远不要定义它们。
算术运算符
一元算术运算符
一元递增和递减运算符有前缀和后缀两种形式。要区分它们,后缀变体需要一个额外的虚拟int参数。如果你重载递增或递减运算符,请确保始终实现前缀和后缀版本。
这是递增的典型实现,递减遵循相同的规则:
struct X {
  X& operator++()
  {
    // Do actual increment
    return *this;
  }
  X operator++(int)
  {
    X tmp(*this);
    operator++();
    return tmp;
  }
};

请注意,后缀变体是基于前缀实现的。还要注意,后缀会多做一次拷贝。
重载一元减号和加号并不常见,最好避免使用。如果需要的话,它们应该作为成员函数进行重载。
另外要注意,后缀变体做的工作更多,因此使用起来比前缀变体效率低。这是普遍偏好前缀递增而不是后缀递增的一个很好的理由。虽然编译器通常可以优化内置类型的后缀递增的额外工作,但对于用户定义的类型(可能是看起来无害的列表迭代器之类的东西),它们可能无法做到同样的优化。一旦你习惯了使用i++,当i不是内置类型时(而且你必须在更改类型时修改代码),很难记住要使用++i,所以最好养成始终使用前缀递增的习惯,除非明确需要后缀递增。
二元算术运算符
对于二进制算术运算符,不要忘记遵守第三个基本规则的运算符重载:如果提供了+,也要提供+=;如果提供了-,不要省略-=,依此类推。据说安德鲁·科尼格是第一个观察到复合赋值运算符可以作为其非复合对应物的基础的人。也就是说,运算符+是基于+=实现的,-是基于-=实现的,依此类推。
根据我们的经验法则,+及其相关运算符应该是非成员函数,而改变左操作数的复合赋值对应物(+=等)应该是成员函数。以下是+=+的示例代码;其他二进制算术运算符应该以相同的方式实现:
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
  }
};

位操作符~ & | ^ << >>应该与算术操作符以相同的方式实现。然而,除了重载<<和>>用于输出和输入之外,很少有合理的用例需要重载这些操作符。
再次强调,从中可以得出的教训是,一般情况下,a += b比a + b更高效,如果可能的话应该优先选择使用a += b。
下标操作符是一个二元操作符,必须作为类成员实现。它用于允许通过键访问其数据元素的类似容器的类型。 提供这些操作符的规范形式如下:
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;
};

请注意,这些也几乎总是需要一个const版本和一个非const版本。 对于->运算符,如果value_type是class(或struct或union)类型,将递归调用另一个operator->(),直到operator->()返回一个非class类型的值。
一元取地址运算符不应该被重载。
对于operator->*(),请参见this question。它很少被使用,因此很少被重载。事实上,即使迭代器也没有重载它。
继续访问转换运算符

111
operator->() 实际上非常奇怪。它不需要返回一个 value_type* —— 事实上,它可以返回另一个类类型,**只要该类类型有一个 operator->()**,那么这个 operator->() 将随后被调用。这种递归调用 operator->() 会一直进行,直到出现 value_type* 返回类型。疯狂的操作! :) - j_random_hacker
4
这并不完全涉及有效性问题,而是在极少数情况下我们不能以传统成语的方式完成操作,因为在计算结果时需要保持两个操作数的定义不变。就像我说的,有两个典型的例子:矩阵乘法和多项式乘法。我们可以用*=来定义*,但这样做会很笨拙,因为*=的第一个操作是创建一个新对象,即计算结果。然后,在for-ijk循环之后,我们会将这个临时对象与*this交换。也就是说:1.复制,2.运算符 *,3.交换。 - Luc Hermitte
9
我不同意您指针类运算符的const/non-const版本,例如const value_type& operator*() const; - 这就像在取消引用时返回T* const以返回const T&一样,这并不是真实情况。或者换句话说:const指针并不意味着const指向物。事实上,模仿T const *并不简单,这也是标准库中整个const_iterator的原因。结论:签名应该是reference_type operator*() const; pointer_type operator->() const - Arne Mertz
6
一条评论:所提出的二进制算术运算符的实现并不像它可以被那样高效。请参考Boost运算符头文件的对称性说明:http://www.boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry如果使用第一个参数的本地副本,执行+=并返回本地副本,则可以避免多余的一次复制。这将启用NRVO优化。 - Manu343726
5
operator<=>() 最终确定下来(在C++20中),比较运算符部分需要进行更新。 - Toby Speight
显示剩余21条评论

548

C++运算符重载的三个基本规则

在C++中进行运算符重载时,应该遵循三个基本规则。像所有规则一样,确实存在例外情况。有时人们偏离这些规则也可以得到不错的代码,但这种积极的偏离是少数。至少我看到的100个偏离中,99个都没有正当理由。然而,实际上可能是1000个偏离中的999个。所以最好遵循以下规则。

  1. 只有当运算符的含义明显清晰时,它才应该被重载。相反,提供一个命名合适的函数。
    基本上,运算符重载的第一条规则就是:不要这样做。这可能看起来很奇怪,因为关于运算符重载有很多知识需要掌握,所以很多文章、书籍章节和其他文本都在处理所有这些内容。但尽管有这些表面上明显的证据,实际上只有极少数情况下适用于运算符重载。原因是除非在应用领域中广泛使用的运算符的用法已经被明确了解并被广泛接受,否则实际上很难理解运算符应用背后的语义。与普遍看法相反,这几乎从未发生过。

  2. 始终坚持这个运算符的良好语义。
    C++对重载运算符的语义没有任何限制。您的编译器将高兴地接受将二元+运算符实现为从其右操作数中减去的代码。但是,这样的运算符的使用者永远不会想到表达式a + b将从b中减去a。当然,这假设运算符在应用领域中的语义是已知且无争议的。

  3. 始终提供一组相关操作的所有输出。
    操作符彼此相关,也与其他操作相关。如果您的类型支持a + b,用户也会期望能够调用a += b。如果它支持前缀递增++a,他们也会期望a++也能正常工作。如果他们可以检查a < b,他们肯定也会希望能够检查a > b。如果他们可以复制构造您的类型,他们期望赋值也能正常工作。


继续阅读成员和非成员之间的决策


17
我所知道唯一违反这些规则的事情是 boost::spirit 哈哈。 - Billy ONeal
79
据一些人说,滥用 + 来进行字符串连接是一种违反规范的行为。不过,由于现在已经成为了一种行业惯例,因此看起来很自然。尽管我还记得在90年代看到一个家庭制作的字符串类使用二进制 & 作为此目的的符号(参考BASIC的惯例)。但是,是的,将其放入标准库基本上已经定下了这个惯例。同样的情况也适用于滥用 <<>> 进行输入输出。为什么左移会成为显而易见的输出操作呢?因为我们在看到第一个“Hello, world!”应用程序时就学习了它,没有其他原因。 - sbi
6
如果你必须解释它,那么它就不是显然清晰和无争议的。同样地,如果你需要讨论或者辩护重载,那么也不是显然清晰和无争议的。 - sbi
6
@sbi: "同行评审" 总是一个好主意。对我来说,选择不当的操作符与选择不当的函数名称没有太大区别(我看到过很多这样的情况)。操作符只是函数,没有更多或更少的含义。规则也是一样的。要了解一个想法是否好,最好的方法就是理解它需要多长时间才能被理解。(因此,同行评审是必须的,但同行必须是没有教条和偏见的人。) - Emilio Garavaglia
6
对我来说,关于operator==唯一绝对明显且无争议的事情是它应该是一个等价关系(即你不应该使用非信号NaN)。容器上有许多有用的等价关系。相等的含义是什么?“a等于b”的意思是ab具有相同的数学值。一个(非NaN)float的数学值的概念是清晰的,但是一个容器的数学值可以有许多不同的(类型递归)有用的定义。最强的相等定义是“它们是相同的对象”,但这是无用的。 - curiousguy
显示剩余39条评论

291
会员与非会员之间的决策
类别 运算符 决策
强制成员函数 [], (), =, ->, ... 成员函数(由C++标准要求)
指向成员的访问 ->* 成员函数
一元运算符 ++, -, *, new, ... 成员函数,除了枚举类型
复合赋值运算符 +=, |=, *=, ... 成员函数,除了枚举类型
其他运算符 +, ==, <=>, /, ... 优先使用非成员函数
二元运算符=(赋值)、[](数组订阅)、->(成员访问)以及n元()(函数调用)运算符必须始终实现为成员函数,因为语言的语法要求如此。
其他运算符可以实现为成员函数或非成员函数。然而,其中一些通常必须实现为非成员函数,因为它们的左操作数不能由您修改。其中最重要的是输入和输出运算符<<和>>,它们的左操作数是标准库中的流类,您无法更改。
对于所有需要选择将其实现为成员函数或非成员函数的运算符,请使用以下经验法则来决定:
  1. 如果它是一个一元运算符,将其实现为成员函数。
  2. 如果一个二元运算符对待两个操作数相等(不改变它们),将该运算符实现为非成员函数。
  3. 如果一个二元运算符对待它的两个操作数相等(通常会改变其左操作数),如果它需要访问操作数的私有部分,将其实现为左操作数类型的成员函数可能会很有用。

当然,像所有经验法则一样,也有例外情况。如果你有一个类型

enum Month {Jan, Feb, ..., Nov, Dec}

如果你想要为枚举类型重载增量和减量运算符,你不能将其作为成员函数来实现,因为在C++中,枚举类型不能拥有成员函数。所以你必须将其作为自由函数来重载。而对于嵌套在类模板中的类模板来说,如果将operator<()作为成员函数内联在类定义中,写起来更容易理解。但这些情况确实很少见。
(然而,如果你做出了一个例外,请不要忘记对操作数的const性质的问题,对于成员函数来说,它变成了隐式的this参数。如果作为非成员函数的运算符将其最左边的参数作为const引用,那么作为成员函数的同一个运算符需要在末尾加上const,以使*this成为一个const引用。)
继续重载常用运算符

12
Herb Sutter在《Effective C++》(或者是《C++编程规范》)中指出,应该优先选择非成员、非友元函数而不是成员函数,以增强类的封装性。个人认为,封装性因素比你的经验法则更为重要,但这并不会降低你经验法则的价值。 - paercebal
11
@paercebal 说的是哪本书?是 Meyers 的《Effective C++》还是 Sutter 的《C++ 编码规范》?无论如何,我不喜欢 operator+=() 不作为成员函数的想法。它需要改变左操作数,因此必须深入其内部实现。如果不将其设置为成员函数,你会得到什么好处呢? - sbi
10
C++编程规范(Sutter)中的第44条建议尽可能编写非成员函数和非友元函数,当然,前提是你能够仅使用类的公共接口来编写该函数。如果不能这样做(或者可以但会严重影响性能),则必须将其设置为成员函数或友元函数。 - Matthieu M.
4
@sbi:哎呀,Effective(有效的)、Exceptional(异常的)……难怪我总是混淆它们的名称。但无论如何,好处在于尽可能地限制可以访问对象私有/保护数据的函数数量。这样,您就可以增加类的封装性,使其更易于维护、测试和演化。 - paercebal
13
举个例子,假设你正在编写一个字符串类,其中包括operator +=append方法。append方法更加完整,因为你可以将参数中从索引 i 到 n - 1 的子字符串追加进去:append(string, start, end)。让+=调用append并将start=0end=string.size似乎是有道理的。那时,append可以是成员方法,但是operator +=不需要是成员,将其变为非成员会减少操作字符串内部的代码量,所以这是一件好事…… ^_^ … - paercebal
显示剩余29条评论

291

C++中运算符重载的一般语法

在C++中,你不能改变内置类型的运算符的含义,运算符只能被重载用于用户自定义类型1。也就是说,至少有一个操作数必须是用户自定义类型。与其他重载函数一样,运算符只能被重载一次,针对一组特定的参数。

并非所有的运算符都可以在C++中被重载。不能被重载的运算符包括:. :: sizeof typeid .*,以及C++中唯一的三元运算符?:

可以在C++中重载的运算符包括以下几种:

类别 运算符 元数和位置
算术 + - * / %+= -= *= /= %= 二元中缀
+ - 一元前缀
++ -- 一元前缀和后缀
位运算 & | ^ << >>&= |= ^= <<= >>= 二元中缀
~ 一元前缀
比较 == != < > <= >= <=> 二元中缀
逻辑 || && 二元中缀
! 一元前缀
分配函数 new new[] delete delete[] 一元前缀
用户定义的转换 T 一元
赋值 = 二元中缀
成员访问 -> ->* 二元中缀
间接/取地址 * & 一元前缀
函数调用 () N元后缀
下标 [] N元2后缀
协程等待 co_await 一元前缀
逗号 , 二元中缀
然而,你可以重载所有这些操作符,并不意味着你应该这样做。请参阅操作符重载的基本规则。
在C++中,操作符以特殊的函数名形式进行重载。与其他函数一样,重载的操作符通常可以实现为左操作数类型的成员函数或非成员函数。选择哪种方式取决于几个标准。对于一个一元操作符@,应用于对象x,可以调用operator@(x)或x.operator@()。对于一个二元中缀操作符@,应用于对象x和y,可以调用operator@(x,y)或x.operator@(y)。
作为非成员函数实现的操作符有时是其操作数类型的友元。

1 “用户定义”这个术语可能有点误导。C++区分内置类型和用户定义类型。前者包括int、char和double;后者包括所有的struct、class、union和enum类型,包括标准库中的类型,尽管它们并非由用户定义。

2 直到C++23,下标运算符一直是二元的,而不是N元的。

3 这在本FAQ的后面部分有详细介绍。

4 C++中的@不是有效的运算符,所以我将其用作占位符。

5 C++中唯一的三元运算符不能重载,而唯一的N元运算符必须始终作为成员函数实现。


继续阅读C++中运算符重载的三个基本规则

2
列表中缺少不可重载运算符 .* - celticminstrel
5
@Mateen 我原本希望使用一个占位符而不是真正的运算符,以明确这不是关于特定运算符,而适用于所有运算符。如果你想成为一名C++程序员,你应该学会关注细节。 :) - sbi
"*" 既被列为不可重载的运算符,也被列为可以重载的杂项之一。 - Rory Yorke
2
@H.R.:如果你读过这个指南,你就会知道问题出在哪里。我通常建议你阅读与问题相关的前三个答案,这应该不会花费你超过半个小时的时间,并且能给你一个基本的理解。你可以稍后查找特定于运算符的语法。你的具体问题表明你尝试将 operator+() 作为成员函数进行重载,但是给它了一个自由函数的签名。请参见这里 - sbi
2
@sbi:我已经阅读了前三篇帖子,感谢您的制作。 :) 我会尝试解决这个问题,否则我认为最好在单独的问题上提问。再次感谢您为我们简化生活! :D - Hosein Rahnama
显示剩余8条评论

189

转换运算符(也称为用户定义的转换)

在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()不是lvalue,所以第一个不匹配:
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: 在函数‘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后隐式转换为整数类型。
继续重载newdelete

167

重载newdelete运算符

注意: 这只涉及重载newdelete语法,而不是其实现。我认为在操作符重载的主题中,重载newdelete值得拥有它们自己的常见问题解答,我永远无法做到完美。

基础知识

在C++中,当你编写new表达式new T(arg)时,这个表达式被评估时会发生两件事情:首先,调用operator new来获取原始内存,然后调用T的适当构造函数将这个原始内存转换为有效对象。同样,当你删除一个对象时,首先会调用它的析构函数,然后将内存返回给operator delete
C++允许你调整这两个操作:内存管理和分配内存时的对象构造/销毁。后者是通过为一个类编写构造函数和析构函数来完成的。调整内存管理则需要编写自己的operator newoperator delete

操作符重载的基本规则之一是-不要这样做,尤其适用于重载newdelete。几乎唯一需要重载这些运算符的原因是出现性能问题内存限制。在许多情况下,像更改所使用的算法这样的其他操作将提供比尝试微调内存管理更高的成本/效益比

C++标准库提供了一组预定义的newdelete运算符。其中最重要的是:

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,即使您从未打算调用它也是如此。原因是,如果构造函数在new表达式求值期间抛出异常,运行时系统将返回内存到operator delete,与调用以分配内存来创建对象的operator new相匹配。如果没有提供匹配的operator delete,则调用默认的operator delete,这几乎总是错误的。
如果重载了new和delete,则还应考虑重载数组变体。

C++允许new和delete操作符接受附加参数。所谓的放置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(); 

请注意,上面所示的放置新代码示例中,只有当X的构造函数引发异常时才不会调用operator delete
您还可以使用其他参数重载newdelete。与放置新的附加参数一样,在关键字new后的括号中列出这些参数。仅出于历史原因,即使它们的参数不是为了在特定地址放置对象,这些变体通常也被称为放置新。
类特定的newdelete 最常见的情况是,您需要微调内存管理,因为测量表明,经常创建和销毁特定类或一组相关类的实例,并且运行时系统的默认内存管理对于这种特定情况效率不高。为了改进这一点,您可以为特定类重载newdelete
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

要重载全局new和delete,只需用我们自己的运算符替换标准库的预定义运算符即可。然而,这很少需要做。


13
我不同意替换全局operator new和delete通常是为了性能优化,相反通常是用于调试bug。 - Yttrill
1
你还应该注意,如果你使用了重载的new运算符,那么你也需要提供一个带有匹配参数的delete运算符。你在全局new/delete部分中提到过这一点,但它并不是很重要。 - Yttrill
14
@Yttrill,你让事情变得混乱了。意思被重载了。所谓的“运算符重载”是指意义被重载了,这并不意味着函数会被字面上地重载,尤其是operator new运算符不会重载标准库版本。@sbi没有声称相反的说法。人们通常将其称为“重载new”,就像通常说“重载加法运算符”一样。 - Johannes Schaub - litb
1
@sbi:请参考(或更好的是链接到)http://www.gotw.ca/publications/mill15.htm。这只是对有时使用`nothrow` new的人的良好实践。 - Alexandre C.
2
如果您没有提供匹配的delete运算符,那么默认的delete运算符将被调用。实际上,如果您添加任何参数并且没有创建匹配的delete运算符,则根本不会调用任何delete运算符,这将导致内存泄漏。(15.2.2,只有在找到适当的...operator delete时才会释放对象占用的存储空间) - dascandy
显示剩余8条评论

56

为什么无法将用于将流式对象插入到 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

3
比较运算符,包括三路比较(C++20)
有相等比较运算符==和!=,以及关系比较运算符<、>、<=、>=。 C++20还引入了三路比较运算符<=>。
操作符 含义和注释(旧版) 含义和注释(C++20)
x == y 如果xy相等,则为真

满足EqualityComparable
(被std::unordered_map使用)
(x <=> y) == 0
(通常直接实现,不委托给三路比较,除非= default)

满足std::equality_comparable
x != y !(x == y) !(x == y)
x < y 如果x小于y,则为真

满足LessThanComparable
(被std::setstd::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

指南

  1. 比较运算符不应该是成员函数。1)
  2. 如果定义了==,也要定义!=(除非在C++20中被重写)。
  3. 如果定义了<,也要定义><=>=
  4. (C++20) 更倾向于定义<=>而不是定义每个关系运算符。
  5. (C++20) 更倾向于默认操作符而不是手动实现。
  6. 相等和关系比较应该匹配,也就是说
    x == y应该等价于!(x < y) && !(y < x)2)
  7. 不要根据<来定义==,即使你可以3)

1) 否则,隐式转换将是不对称的,而且预期==会对两边应用相同类型的隐式转换。
2) 这种等价性不适用于float,但适用于int和其他强有序类型。
3) 这是为了提高可读性、正确性和性能。

在C++20之前的实现和常见惯用法

免责声明
如果您正在使用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); }
};

注意:在C++11中,所有这些通常可以使用noexceptconstexpr
如果我们有一个部分有序的成员(例如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中已弃用)将!=><=>=委托给某个作用域中所有类型的<==


默认比较(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

参见cppreference article on default comparison
表达式重写(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)
注意:它在口语中被称为“太空船运算符”。另请参阅
`x <=> y`的基本思想是,结果告诉我们`x`是小于、大于、等于还是与`y`无序。 这类似于C语言中的`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可以转换为std::weak_ordering,而std::weak_ordering可以转换为std::partial_ordering。 这些类别的值可以进行比较(例如(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;
};

如果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来比较数组成员。

1

规范函数签名摘要

许多运算符重载可以返回几乎任何东西。例如,在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
};

请注意,自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*);
// ...

-3
简单明了地说,我将提到一些我在过去一周学习Python和C++、面向对象编程以及其他方面时遇到的一些问题,如下所示:
  1. 运算符的元数不能进一步修改!

  2. 重载运算符只能有一个默认参数,函数调用运算符则不能。

  3. 只有内置运算符可以被重载,其他运算符不能。

更多信息,请参考以下链接,该链接将引导您访问GeekforGeeks提供的文档。

https://www.geeksforgeeks.org/g-fact-39/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接