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

标签 c++ operators operator-overloading c++-faq

注意:答案是按照特定顺序给出的,但是由于许多用户是根据投票而不是给出时间来对答案进行排序的,因此以下是最有意义的顺序的索引:


The General Syntax of operator overloading in C++
The Three Basic Rules of Operator Overloading in C++
The Decision between Member and Non-member
Common operators to overload


赋值运算符
输入和输出运算符
函数调用运算符
比较运算符
算术运算符
数组下标
指针类型的运算符
Conversion Operators

Overloading new and delete



(注意:这应该是Stack Overflow's C++ FAQ的条目。如果您想批评以这种形式提供FAQ的想法,那么the posting on meta that started all this就是这样做的地方。 C++ chatroom,这是FAQ想法首先出现的地方,因此,您的答案很可能会被提出该想法的人阅读。)

最佳答案

普通运算符重载

重载操作员中的大部分工作是样板代码。这也就不足为奇了,由于运算符仅仅是语法糖,它们的实际工作可以由(通常转发给)普通函数来完成。但是,重要的是要正确编写此样板代码。如果失败,则操作员的代码将无法编译,或者用户的代码将无法编译,或者用户的代码将表现异常。

赋值运算符

关于任务有很多要说的。但是,大多数内容已经在GMan's famous Copy-And-Swap FAQ中说过了,因此在这里我将跳过大部分内容,仅列出完美的赋值运算符以供参考:

X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}


位移位运算符(用于流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;
}


实施operator>>时,仅当读取本身成功时才需要手动设置流的状态,但是结果不是预期的。

函数调用运算符

必须将用于创建函数对象(也称为函子)的函数调用运算符定义为成员函数,因此它始终具有成员函数的隐式this参数。除此之外,可以重载任何数量的附加参数,包括零。

这是语法示例:

class foo {
public:
    // Overloaded call operator
    int operator()(const std::string& y) {
        // ...
    }
};


用法:

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


在整个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);}


这里要注意的重要一点是,这些运算符中只有两个实际执行任何操作,其他运算符只是将其参数转发给这两个运算符中的任何一个以进行实际工作。

重载其余二进制布尔运算符(||&&)的语法遵循比较运算符的规则。但是,您不太可能为这些找到合理的用例2。

1与所有经验法则一样,有时也可能有理由打破这一原则。如果是这样,请不要忘记二进制比较运算符的左操作数(对于成员函数而言,该运算符也将是*this)也必须是const。因此,实现为成员函数的比较运算符必须具有以下签名:

bool operator<(const X& rhs) const { /* do actual comparison with *this */ }


(请注意最后的const。)

2应该注意,||&&的内置版本使用快捷方式语义。虽然用户定义的语法(因为它们是方法调用的语法糖),却不使用快捷方式语义。用户将期望这些运算符具有捷径语义,并且它们的代码可能依赖于此,因此,强烈建议不要定义它们。

算术运算符

一元算术运算符

一元递增和递减运算符具有前缀和后缀形式。为了彼此区分,postfix变体采用了另一个哑int参数。如果您使增量或减量过载,请确保始终同时实现前缀和后缀版本。
这是递增的规范实现,递减遵循相同的规则:

class X {
  X& operator++()
  {
    // do actual increment
    return *this;
  }
  X operator++(int)
  {
    X tmp(*this);
    operator++();
    return tmp;
  }
};


请注意,后缀变体是根据前缀实现的。另请注意,后缀会额外复制2。

一元负号和加号的重载不是很常见,最好避免。如果需要,它们可能应该作为成员函数重载。

2还请注意,postfix变体比前缀变体执行更多的工作,因此使用效率较低。这是一个很好的理由,通常优先选择前缀增量而不是后缀增量。尽管编译器通常可以优化内置类型的后缀增量的其他工作,但对于用户定义的类型,它们可能无法做到相同(这可能看起来像列表迭代器一样无辜)。一旦习惯了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+通过复制而不是通过const引用获取其左操作数。这样做的原因与operator=每个副本采用其参数的原因相同。

位操作运算符~ & | ^ << >>应该以与算术运算符相同的方式实现。但是,(除了重载<<>>的输出和输入),很少有合理的用例可以重载这些用例。

3再次,从中可以得出的教训是,a += b通常比a + b更有效,并且如果可能的话应该首选。

数组下标

数组下标运算符是二进制运算符,必​​须将其实现为类成员。它用于类容器类型,允许通过键访问其数据元素。
提供这些的规范形式是这样的:

class X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) const;
  // ...
};


除非您不希望您的类的用户能够更改operator[]返回的数据元素(在这种情况下,您可以忽略non-const变体),否则应始终提供运算符的两个变体。

如果已知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;
};


注意,这些也几乎总是需要const版本和非const版本。
对于->运算符,如果value_typeclass(或structunion)类型,则递归调用另一个operator->(),直到operator->()返回非类类型的值。

一元地址运算符绝对不能重载。

对于operator->*(),请参见this question。它很少使用,因此也很少过载。实际上,即使迭代器也不会使它过载。



继续Conversion Operators

关于c++ - 运算符重载的基本规则和惯用法是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39502352/

相关文章:

c++ - 面向对象设计 : Multiple instances but Static Callbacks

c++ - 此代码中是否存在数据竞争?

c++ - ->、->* 和 .* 运算符的正确术语是什么?

C# 扩展数组类型以重载运算符

c++ - vector 上的运算符 += 失败

c++ - 重载运算符 delete 可以有默认参数吗?

c++ - OpenGL VBO批处理最佳做法

c++ - 如果 Q_OBJECT 宏被其他宏删除,则无法编译 MOC 文件 : The header file doesn't include <QObject>

具有不同类型的 C++ 对称二元运算符

c++ - g++ L"string~"+ 运算符,类似于 Visual C++