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

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)

Bitshift运算符(用于流I/O)

位移操作符提升<<>>,虽然在硬件接口用于它们选自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++,很难记住,++ii不是内置类型时(更改类型时你必须更改代码),所以最好养成一个习惯使用前缀增量,除非明确需要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(或structunion)类型,另一个operator->()被递归调用,直到operator->()返回非类类型的值.

一元地址运算符永远不应该重载.

对于operator->*()这个问题.它很少使用,因此很少超载.事实上,即使是迭代器也不会使它超载.


继续转换操作员

  • `operator - >()`实际上*非常奇怪.它不需要返回一个`value_type*` - 实际上,它可以返回另一个类类型,**只要类类型有一个`operator - >()`**,然后随后将调用它.这种`operator - >()的递归调用继续进行,直到出现`value_type*`返回类型.疯狂!:) (86认同)
  • 一条评论:建议的二进制算术运算符的实现并不是那么有效.Se Boost运算符标题simmetry注意:http://www.boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry如果使用第一个参数的本地副本,可以避免多一个副本, + =,并返回本地副本.这样可以实现NRVO优化. (6认同)
  • 我不同意你的指针式运算符的const/non-const版本,例如`const value_type&operator*()const;` - 这就像有一个`T*const`在dereferencing上返回一个`const T&`,事实并非如此.或者换句话说:const指针并不意味着const指针.事实上,模仿`T const*` - 这是标准库中整个`const_iterator`内容的原因并非易事.结论:签名应该是`reference_type operator*()const; pointer_type operator - >()const` (5认同)
  • 正如我在聊天中提到的,`L <= R`也可以表示为`!(R <L)`而不是`!(L> R)`.可能会在难以优化的表达式中保存额外的内联层(这也是Boost.Operators实现它的方式). (3认同)
  • @UKMonkey:你为什么不按照我提供的链接?在那里,GMan痛苦地解释了为什么你这个假设大约落后于现有技术10 - 15年. (3认同)
  • 请注意,如果不切实际,有些情况下会根据 `operatorX=` 定义 `operatorX`。对于矩阵和多项式(英语?),乘法的代码分解是另一种方式:`operator*=` 应该根据 `operator*` 定义。 (2认同)
  • 这不完全是关于有效性的。这是关于我们在极少数情况下不能以传统惯用的方式做到这一点:当在计算结果时两个操作数的定义都需要保持不变时。正如我说的,有两个经典示例:矩阵乘法和多项式乘法。我们可以用**来定义*,但是这很尴尬,因为**的第一个操作之一是创建一个新对象,即计算结果。然后,在for-ijk循环之后,我们将这个临时对象与* this交换。即。1.复制,2.运算符*,3.交换 (2认同)
  • @thomthom:如果一个类没有可公开访问的API来获取其状态,则必须使需要访问其状态的所有内容都成为该类的成员或“朋友”。当然,这对所有运营商都是如此。 (2认同)
  • @jbi http://juanchopanzacpp.wordpress.com/2014/05/11/want-speed-dont-always-pass-by-value/ (2认同)
  • 关于比较运算符的部分将需要更新,以在最终版本(C ++ 20)时提及`operator &lt;=&gt;()`。 (2认同)
  • @安德鲁:那就是死法(尽管在提出这个成语时,甚至在纸上甚至还没有移动构造)。 (2认同)
  • @MaxPower 我不再活跃于 SO 了。我知道这已经过时了。如果与“operator&lt;=&gt;”相关的问题被作为一个骗局而关闭,那么这当然是错误的,但让我离开的问题之一是普通用户几乎完全无力解决这些问题。没有在提供此运算符的平台上工作,因此没有任何经验,我不会添加任何相关内容。对不起。你想让我做什么? (2认同)

sbi*_*sbi 481

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

当谈到C++中的运算符重载时,您应该遵循三个基本规则.与所有此类规则一样,确实存在例外情况.有时人们偏离了他们,结果并不是糟糕的代码,但这种积极的偏差很少.至少,我所看到的100个这样的偏差中有99个是没有道理的.但是,它可能只有1000中的999.所以你最好坚持以下规则.

  1. 只要操作员的意义不明显且无可争议,就不应该超载. 相反,提供具有良好选择名称的功能.
    基本上,重载运营商的第一个也是最重要的规则是:不要这样做.这可能看起来很奇怪,因为有很多关于运算符重载的知识,因此很多文章,书籍章节和其他文本都涉及到这一切.但尽管有这些看似明显的证据,但只有极少数情况下运营商超载是合适的.原因是实际上很难理解运算符应用背后的语义,除非在应用程序域中使用运算符是众所周知且无可争议的.与普遍看法相反,情况并非如此.

  2. 始终坚持运营商众所周知的语义.
    C++对重载运算符的语义没有限制.您的编译器将很乐意接受实现二元+运算符的代码从其右操作数中减去.然而,这样的运营商的用户将不会怀疑表达a + b减去ab.当然,这假设应用程序域中的运算符的语义是无可争议的.

  3. 始终提供一系列相关操作.
    运营商彼此之间以及与其他运营相关.如果您的类型支持a + b,用户也可以打电话a += b.如果它支持前缀增量++a,它们也a++可以工作.如果他们可以检查是否a < b,他们肯定也希望能够检查是否a > b.如果他们可以复制构造您的类型,他们希望分配也可以工作.


继续执行会员与非会员之间的决定.

  • @Billy:根据一些人的观点,滥用"+"进行字符串连接是一种违规行为,但它现在已成为完善的实践,因此看起来很自然.虽然我确实记得我在90年代看到的家庭酿造字符串类,它使用二进制`&`用于此目的(指BASIC用于已建立的实践).但是,是的,把它放入std lib基本上就是这样.对于IO,BTW滥用`<<`和`>>`也是如此.为什么左移是明显的输出操作?因为当我们看到第一个"你好,世界!"时我们都学到了它.应用.而且没有其他原因. (66认同)
  • 我所知道的唯一违反其中任何一项的是"boost :: spirit"大声笑. (15认同)
  • @curiousguy:如果你必须解释它,它显然并不明显且无可争议.同样,如果您需要讨论或捍卫超载. (5认同)
  • @sbi:"同行评审"总是一个好主意.对我来说,一个选择不当的操作符与选择不当的函数名称没什么不同(我看到很多).操作员只是功能.不多也不少.规则是一样的.要了解一个想法是否良好,最好的方法是了解需要多长时间才能被理解.(因此,同行评审是必须的,但必须在没有教条和偏见的人之间选择同伴.) (5认同)
  • @sbi对我来说,关于`operator ==`的唯一绝对明显且无可争议的事实是它应该是一个等价关系(IOW,你不应该使用非信令NaN).容器上有许多有用的等价关系.平等意味着什么?"`a`等于`b`"表示`a`和`b`具有相同的数学值.(非NaN)浮点数的数学概念是明确的,但容器的数学值可以有许多不同的(类型递归)有用的定义.平等的最强定义是"它们是相同的对象",它是无用的. (5认同)
  • @Daniel:看,如果没有人为IO过载"<<"和">>",你现在想出来了,那就不好意思了.自从它在30年前完成以来,它现在已成为该领域的一部分.对于其他一些不常用的东西也是如此:如果你或我在我们的项目中这样做,很可能我们的同事会抱怨.如果它是在一个流行的boost库中完成的,并且适合代码的域(使用`|`来管道是众所周知的),这可能会有所不同. (4认同)
  • @curiousguy:想到`std :: auto_ptr`上的`operator =`. (3认同)
  • @curiousguy:该语言允许将'operator +'定义为减法.这是一个非常糟糕的主意,因为它令人惊讶.`x = y;`不应该改变`y`.对于任何内置类型,它都不会这样做.需要支持`C :: operator =(C&)`来支持引用计数指针之类的东西,其中内部状态可能会改变但用户可见状态会改变.`std :: auto_ptr`被普遍认为是一个错误. (3认同)
  • 这*似乎*很有道理,但有缺点:第1点将使boost :: spirit不可能存在,同样适用于iostream(cout <<"hello world"移位?)Point2字面上禁止任何创新:某事刚发明的并不是"众所周知的",但我将来也会如此.std :: string use + not to"add".而且他们在STL发明之前没有"众所周知". (2认同)
  • @curiousguy:这就是 Java/C# 方法。在 C++ 中,容器保存 _values_,而不是 _references_,因此“相等”和“等价”之间的区别没有实际意义。(如果我的差异是正确的,那就是。) (2认同)
  • @sbi:从概念上讲是反对的,但认识到一个好处。从本质上讲,我不相信运算符和函数之间的区别:如果+表示“串联”或一元*表示“敏锐的星星”或〜表示“正交”或“转置”,则std :: string :: empty为也是可疑的(用于检查状态的命令性动词?!?难道不是is_empty吗?)。(...) (2认同)
  • (...)真正的事实是,当你是初学者时,第一个重载yopu的样本看是`cout <<"hello world"`,这只是对规则的违反(```应该移位!)被老师隐藏的谎言是"插入操作员".现在1只能是真的:1)"插入操作符"是一个碱液,并且违反了规则的第一课(LOL!)或...... 2)`<<`只是一个符号的含义取决于其应用的顶部,因此不应存在对其含义的偏见.(......) (2认同)
  • @curiousguy:不,您正在使这种方式变得复杂。在异构容器(也称为“ struct”)上对`operator ==()`的明显实现是,对于所有元素(又称为“成员”),`operator ==()`应用于相应的容器时均返回true另一个相同类型的“ struct”中的元素。非异构容器的“ operator ==()”的明显含义是,对于容器中的所有元素,“ operator ==()”在应用于相同类型的另一个容器中的对应对象时将返回true。 (2认同)
  • @sbi:你的所有论点都会减少为"除非别人已多次这样做,否则不要这样做".将此应用于"生孩子"和"任何人",人类将在一代人的时间内熄灭!这是一项艰苦的工作,但有人必须开始.新手或专家,不是重点.*想法*就是重点.如果有足够的"信徒"这个想法,否则就会死亡.不要对新手说"种族主义":反而告诉他们"远离水",教他们"如何游泳"!(你可能会感到惊讶!) (2认同)
  • @Emilio:是的,我对寻求建议的人的建议是不要发明新的东西。在过去的20年中,我看到了很多滥用操作员重载的现象,并且很少有合理的使用方法。(而且,在我使用C ++的最初几年中,我也对此感到内.。)因此,我的建议是坚持使用众所周知的范例,除非您对运算符重载有足够的了解,因此在第一章中就不需要建议地点。(关于您的其余论点:比较结果太短了,我不会花时间在上面。) (2认同)
  • 是的,我同意 cout &lt;&lt; "Hello world" 是对这些规则最严重的违反。我确信它的实现是为了演示运算符重载可以做什么,我们从未打算在现实世界中使用。 (2认同)
  • @Robinson:STL将容器的比较定义为词典比较,而不是长度.因此,每个看着"v1 <v2"的人都会期望它也这样做.对于你想要的,有`v1.size()<v2.size()`. (2认同)
  • @Moiz:如果您想知道编译器是否默认生成赋值运算符,请参阅[this question](http://stackoverflow.com/q/3734247/140719)及其答案. (2认同)

sbi*_*sbi 258

C++中运算符重载的通用语法

您无法在C++中更改内置类型的运算符的含义,只能为用户定义的类型1重载运算符.也就是说,至少一个操作数必须是用户定义的类型.与其他重载函数一样,运算符只能为一组参数重载一次.

并非所有运算符都可以在C++中重载.在无法重载的运算符中有:. :: sizeof typeid .*和C++中唯一的三元运算符,?:

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

  • 算术运算符:+ - * / %+= -= *= /= %=(所有二进制中缀); + -(一元前缀); ++ --(一元前缀和后缀)
  • 位操作:& | ^ << >>&= |= ^= <<= >>=(所有二进制中缀); ~(一元前缀)
  • boolean algebra :( == != < > <= >= || &&所有二进制中缀); !(一元前缀)
  • 内存管理: new new[] delete delete[]
  • 隐式转换运算符
  • miscellany :( = [] -> ->* , 所有二进制中缀); * &(所有一元前缀)()(函数调用,n-ary中缀)

但是,你可以超载所有这些并不意味着你应该这样做.请参阅运算符重载的基本规则.

在C++中,运算符以具有特殊名称函数形式重载.为具有其他功能,重载操作符可以通常被实现为一个其左操作数的类型的成员函数或作为非成员函数.您是否可以自由选择或限制使用其中任何一个取决于几个标准.2应用于对象x的一元运算符@3可以作为operator@(x)或作为调用x.operator@().二元缀运算符@,施加到对象xy被称为或者作为operator@(x,y)或作为x.operator@(y).4

实现为非成员函数的运算符有时是其操作数类型的朋友.

1 "用户定义"一词可能略有误导.C++区分内置类型和用户定义类型.前者属于例如int,char和double; 后者属于所有struct,class,union和enum类型,包括来自标准库的类型,即使它们不是由用户定义的.

2 这是覆盖在后面的部分这个常见问题.

3 @不是C++中的有效运算符,这就是我将其用作占位符的原因.

4 C++中唯一的三元运算符不能重载,唯一的n-ary运算符必须始终作为成员函数实现.


继续使用C++中的运算符重载的三个基本规则.

  • `%=`不是"位操作"运算符 (5认同)
  • @Mateen 我 _wanted_ 使用占位符而不是真正的运算符,以明确这与特殊运算符无关,而是适用于所有运算符。而且,如果你想成为一名 C++ 程序员,你应该学会注意小字体。`:)` (3认同)
  • 不可重载运算符列表中缺少 `.*`。 (2认同)
  • @HR:如果您阅读了本指南,您就会知道出了什么问题。我通常建议您阅读问题中链接的前三个答案。这不应该超过你生活中的半小时,并让你有一个基本的了解。您可以稍后查找特定于运算符的语法。您的具体问题建议您尝试将 `operator+()` 作为成员函数重载,但给它一个自由函数的签名。请参阅[此处](/sf/answers/309521061/)。 (2认同)
  • @sbi:我已经阅读了前三篇文章,感谢您制作它们。:) 我会尝试解决问题,否则我认为最好在单独的问题上提问。再次感谢您让我们的生活变得如此轻松!:D (2认同)

sbi*_*sbi 240

会员与非会员之间的决定

二元运算符=(赋值),[](数组预订),->(成员访问)以及n-ary ()(函数调用)运算符必须始终作为成员函数实现,因为语言的语法要求它们.

其他运营商可以作为成员或非成员实施.但是,其中一些通常必须作为非成员函数实现,因为它们的左操作数不能被您修改.最突出的这些是输入和输出操作符<<>>,它的左操作数是从你不能改变标准库流类.

对于您必须选择将它们实现为成员函数或非成员函数的所有运算符,请使用以下经验法则来决定:

  1. 如果它是一元运算符,则将其实现为成员函数.
  2. 如果二元运算符同等地处理两个操作数(它保持不变),则将此运算符实现为非成员函数.
  3. 如果二元运算并没有把它的两个操作数相等(通常它会改变其左操作数),它可能是使它成为一个有用的成员其左操作数的类型的函数,如果它有访问操作数的私处.

当然,与所有经验法则一样,也有例外.如果你有类型

enum Month {Jan, Feb, ..., Nov, Dec}
Run Code Online (Sandbox Code Playgroud)

并且你想为它重载递增和递减运算符,你不能将它作为成员函数来执行,因为在C++中,枚举类型不能具有成员函数.所以你必须将它作为一个自由函数重载.和operator<()嵌套类模板中的类模板是写起来更简单,当作为类定义的成员函数内联方式完成阅读.但这些确实是罕见的例外.

(但是,如果你做了一个例外,不要忘记const操作数的-ness 问题,对于成员函数,成为隐式this参数.如果作为非成员函数的操作符将其最左边的参数作为const参考中,相同的操作者作为成员函数需要具有const在端部,使*thisconst参考).


继续向Common运算符重载.

  • @sbi:一个例子.假设您使用`operator + =`和`append`方法编写String类.`append`方法更完整,因为你可以将参数的子字符串从索引i追加到索引n -1:`append(string,start,end)`将`+ =`调用追加到`似乎是合乎逻辑的start = 0`和`end = string.size`.在那一刻,append可以是一个成员方法,但是`operator + =`不需要是成员,并且使它成为非成员会减少使用String内部的代码数量,所以它是一个好的事情...... ^ _ ^ ...... (12认同)
  • Herb Sutter在Effective C++(或者是C++编码标准?)中的项目表示,应该更喜欢非成员非友元函数到成员函数,以增加类的封装.恕我直言,封装原因优先于您的经验法则,但它不会降低您的经验法则的质量值. (9认同)
  • @sbi:C++编码标准中的第44项(Sutter)**首选编写非成员非友好函数**,当然,它只适用于只使用类的公共接口实际编写此函数的情况.如果你不能(或者可以,但它会严重阻碍性能),那么你必须让它成为会员或朋友. (9认同)
  • @paercebal:_Effective C++ _由Meyers撰写,_C++ Coding Standards_由Sutter撰写.你指的是哪一个?无论如何,我不喜欢`operator + =()`不是成员的想法.它必须改变它的左手操作数,因此根据定义它必须深入挖掘其内部.如果不成为会员,你会得到什么? (7认同)
  • @sbi:哎呀,有效,特殊......难怪我把这些名字混在一起.无论如何,增益是尽可能地限制有权访问对象私有/受保护数据的函数的数量.这样,您可以增加类的封装,使其维护/测试/演变更容易. (3认同)
  • @sbi :交换性不应该是一个标准。例如,矩阵乘法不是可交换的,而数字乘法是。我是否应该仅仅因为 C++ 原生整数 `*` 是可交换的就禁止为矩阵重载 `*`?不。这里适用的是最小意外原则,我们必须根据其上下文(即其参数)来分析运算符。字符串和运算符`+`(以及扩展名`+=`)也是如此。事实是,没有人期望 `+` 运算符在字符串上是可交换的,那么,这有什么问题呢? (2认同)
  • @sbi:现在,关于运算符的现有实践可以应用于接口(例如,在字符串上提供`+`和`+ =`运算符,因为用户确实期望它们),它不应该用于决定实现细节IMHO(例如决定`+ =`是成员还是非成员).这个实现细节不是关于个人的品味偏好:减少访问私有数据的代码量(即封装)是一件好事,并且可以测量,因此应该尽可能地追求(只要代码保持清晰) ,当然可读和正确) (2认同)
  • @paercebal 有一些缺失的点。1. `friend` 有助于仅 ADL 查找,因此减少命名空间范围的污染。尽管存在非公共成员,这仍然有效。因此,让某些东西尽可能成为非成员并不是那么惯用。2. 非成员“operator+=”的简单重载无法在允许修改的纯右值上工作,但成员重载却可以工作。3. 对非成员超载故障的诊断可能是一场灾难。通常对于会员来说并不是那么糟糕。 (2认同)

JKo*_*Kor 159

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

在C++中,您可以创建转换运算符,这些运算符允许编译器在您的类型和其他已定义类型之间进行转换.有两种类型的转换运算符,隐式和显式运算符.

隐式转换运算符(C++ 98/C++ 03和C++ 11)

的隐式转换运算符允许编译器隐式转换(像之间的转换intlong用户定义的类型的值),以一些其它类型的.

以下是一个带隐式转换运算符的简单类:

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++程序员有时会感到惊讶,因为编译器选择了他们没有怀疑的过载.显式转换运算符可以减轻这些问题.

显式转换运算符(C++ 11)

与隐式转换运算符不同,显式转换运算符在您不期望它们时将永远不会启动.以下是具有显式转换运算符的简单类:

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之后,不允许编译器隐式转换为整数类型.

继续加载newdelete.


sbi*_*sbi 145

重载newdelete

注意:这只涉及重载的语法,newdelete不是这些重载运算符的实现.我认为重载的语义newdelete配得上自己的常见问题解答,在运算符重载的主题内我永远无法做到公道.

基本

在C++中,当你写一个新的表达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(); 
Run Code Online (Sandbox Code Playgroud)

前两个为对象分配/释放内存,后两个为对象数组.如果您提供自己的版本,它们不会超载,而是替换标准库中的那些.
如果你重载operator new,你应该总是重载匹配operator delete,即使你从不打算调用它.其原因在于,如果一个构造一个新的表达式的计算过程中抛出,运行时系统将内存返回operator delete匹配operator new的是被称为分配给创建的对象的内存.如果你不提供匹配operator delete,调用默认值,这几乎总是错误的.
如果你超载newdelete,你应该考虑超载数组变量了.

放置 new

C++允许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的构造函数抛出异常.

您也可以重载newdelete使用其他参数.与放置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,只需用我们自己的标准库替换标准库的预定义运算符.但是,这很少需要完成.

  • @Yttrill你是混乱的事情.*含义*过载."运算符重载"意味着意义是重载.这并不意味着字面上的函数被重载,*特别是*operator new不会超出Standard的版本.@sbi没有声称相反.通常称其为"重载新",因为通常会说"重载加法运算符". (13认同)
  • 我也不同意将全局运算符new和delete替换为性能:相反,它通常用于跟踪错误. (11认同)
  • “如果您没有提供匹配的删除操作符,则调用默认操作符” -&gt; 实际上,如果您添加任何参数并且不创建匹配的删除操作,则根本不会调用操作符删除,并且会出现内存泄漏。(15.2.2,只有找到合适的...运算符delete,对象占用的存储才会被释放) (2认同)

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)

这非常直观.


Jan*_*tke 6

比较运算符,包括三向比较(C++20)

相等比较 ==!=,以及关系比较 <, >, <=, >=。C++20还引入了三路比较运算符<=>

操作员 含义和注释(旧) 含义和注释 (C++20)
x == y true 如果xy相等

满足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_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 部分。

所有运算符通常都实现为非成员函数,可能作为隐藏的友元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 中,所有这些通常都可以是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; }
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 中已弃用)将!=><=、 和委托>=给某个范围内的所有类型。<==


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

请参阅有关默认比较的 cppreference 文章

表达式重写 (C++20)

在 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)

注意:constexprnoexcept是可选的,但几乎总是可以应用于比较运算符。

三路比较运算符 (C++20)

注:俗称“宇宙飞船操作员”。另请参见

背后的基本思想x <=> y是,结果告诉我们是否x低于、大于、等于或无序ystrcmp这与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)

比较类别

该运算符的结果不是boolint,而是比较类别的值。

比较类别 例子 可能的值
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 次

最近记录:

6 年,6 月 前