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

注意:答案是按特定顺序给出的,但由于许多用户根据投票而不是给出的时间对答案进行排序,这里有一个 答案按最有意义的顺序排列:

Note: The answers were given in a specific order, but since many users sort answers according to votes, rather than the time they were given, here's an index of the answers in the order in which they make the most sense:

  • C++ 中运算符重载的一般语法
  • C++中运算符重载的三个基本规则
  • 会员与非会员之间的决定
  • 常用运算符重载
    • 赋值运算符
    • 输入和输出运算符
    • 函数调用运算符
    • 比较运算符
    • 算术运算符
    • 数组下标
    • 类指针类型的运算符

    (注意:这是Stack Overflow 的 C++ 常见问题解答的一个条目. 如果您想批评以这种形式提供常见问题解答的想法,那么 开始这一切的元数据发布 将是这样做的地方.该问题的答案在 C++ 聊天室,FAQ 想法最初就是从这里开始的,所以你的回答很可能会被提出这个想法的人阅读.)

    (Note: This is meant to be an entry to Stack Overflow's C++ FAQ. If you want to critique the idea of providing an FAQ in this form, then the posting on meta that started all this would be the place to do that. Answers to that question are monitored in the C++ chatroom, where the FAQ idea started in the first place, so your answer is very likely to get read by those who came up with the idea.)

    推荐答案

    常用运算符重载

    重载运算符的大部分工作是样板代码.这并不奇怪,因为运算符只是语法糖,它们的实际工作可以通过(并且通常被转发到)普通函数来完成.但重要的是你要正确地获得这个样板代码.如果您失败了,要么您的操作员代码无法编译,要么您的用户代码无法编译,或者您的用户代码行为异常.

    Common operators to overload

    Most of the work in overloading operators is boiler-plate code. That is little wonder, since operators are merely syntactic sugar, their actual work could be done by (and often is forwarded to) plain functions. But it is important that you get this boiler-plate code right. If you fail, either your operator’s code won’t compile or your users’ code won’t compile or your users’ code will behave surprisingly.

    关于作业有很多话要说.但是,大部分内容已经在 GMan 著名的 Copy-And-Swap 常见问题解答中说过,所以这里我就跳过大部分,只列出完美的赋值运算符以供参考:

    There's a lot to be said about assignment. However, most of it has already been said in GMan's famous Copy-And-Swap FAQ, so I'll skip most of it here, only listing the perfect assignment operator for reference:

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

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

    位移位运算符 <<>> 虽然仍用于硬件接口以实现它们从 C 继承的位操作功能,但已变得越来越多在大多数应用程序中作为重载流输入和输出运算符普遍存在.有关作为位操作运算符的重载指南,请参阅下面有关二元算术运算符的部分.要在您的对象与 iostream 一起使用时实现您自己的自定义格式和解析逻辑,请继续.

    Bitshift Operators (used for Stream I/O)

    The bitshift operators << and >>, although still used in hardware interfacing for the bit-manipulation functions they inherit from C, have become more prevalent as overloaded stream input and output operators in most applications. For guidance overloading as bit-manipulation operators, see the section below on Binary Arithmetic Operators. For implementing your own custom format and parsing logic when your object is used with iostreams, continue.

    在最常见的重载运算符中,流运算符是二进制中缀运算符,其语法没有指定它们应该是成员还是非成员.由于它们改变了左参数(它们改变了流的状态),根据经验法则,它们应该被实现为左操作数类型的成员.但是,它们的左操作数是来自标准库的流,虽然标准库定义的大多数流输出和输入操作符确实定义为流类的成员,但是当您为自己的类型实现输出和输入操作时,您不能更改标准库的流类型.这就是为什么你需要为你自己的类型实现这些操作符作为非成员函数.两者的规范形式如下:

    The stream operators, among the most commonly overloaded operators, are binary infix operators for which the syntax specifies no restriction on whether they should be members or non-members. Since they change their left argument (they alter the stream’s state), they should, according to the rules of thumb, be implemented as members of their left operand’s type. However, their left operands are streams from the standard library, and while most of the stream output and input operators defined by the standard library are indeed defined as members of the stream classes, when you implement output and input operations for your own types, you cannot change the standard library’s stream types. That’s why you need to implement these operators for your own types as non-member functions. 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>>时,只有在读取成功时才需要手动设置流的状态,但结果不是预期的.

    When implementing operator>>, manually setting the stream’s state is only necessary when the reading itself succeeded, but the result is not what would be expected.

    函数调用运算符,用于创建函数对象,也称为函子,必须定义为成员函数,所以它总是有隐含的this 成员函数的参数.除此之外,它可以被重载以接受任意数量的附加参数,包括零.

    The function call operator, used to create function objects, also known as functors, must be defined as a member function, so it always has the implicit this argument of member functions. Other than this, it can be overloaded to take any number of additional arguments, including zero.

    这是一个语法示例:

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

    用法:

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

    在整个 C++ 标准库中,函数对象总是被复制的.因此,您自己的函数对象复制起来应该很便宜.如果函数对象绝对需要使用复制成本高昂的数据,最好将该数据存储在其他地方并让函数对象引用它.

    Throughout the C++ standard library, function objects are always copied. Your own function objects should therefore be cheap to copy. If a function object absolutely needs to use data which is expensive to copy, it is better to store that data elsewhere and have the function object refer to it.

    根据经验法则,二进制中缀比较运算符应作为非成员函数实现1.一元前缀否定 ! 应该(根据相同的规则)作为成员函数实现.(但重载它通常不是一个好主意.)

    The binary infix comparison operators should, according to the rules of thumb, be implemented as non-member functions1. The unary prefix negation ! should (according to the same rules) be implemented as a member function. (but it is usually not a good idea to overload it.)

    标准库的算法(例如std::sort())和类型(例如std::map)总是只需要operator<代码> 存在.但是,您类型的用户也会期望所有其他运算符都存在,因此如果您定义 operator<,请务必遵循运算符的第三条基本规则重载并定义所有其他布尔比较运算符.实现它们的规范方法是这样的:

    The standard library’s algorithms (e.g. std::sort()) and types (e.g. std::map) will always only expect operator< to be present. However, the users of your type will expect all the other operators to be present, too, so if you define operator<, be sure to follow the third fundamental rule of operator overloading and also define all the other boolean comparison operators. The canonical way to implement them is this:

    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);}
    

    这里要注意的重要一点是,这些运算符中只有两个实际上做了任何事情,其他的只是将他们的参数转发给这两个中的任何一个来完成实际的工作.

    The important thing to note here is that only two of these operators actually do anything, the others are just forwarding their arguments to either of these two to do the actual work.

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

    The syntax for overloading the remaining binary boolean operators (||, &&) follows the rules of the comparison operators. However, it is very unlikely that you would find a reasonable use case for these2.

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

    1 As with all rules of thumb, sometimes there might be reasons to break this one, too. If so, do not forget that the left-hand operand of the binary comparison operators, which for member functions will be *this, needs to be const, too. So a comparison operator implemented as a member function would have to have this signature:

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

    (注意末尾的 const.)

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

    2 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 参数.如果重载增量或减量,请确保始终实现前缀和后缀版本.这是增量的规范实现,减量遵循相同的规则:

    The unary increment and decrement operators come in both prefix and postfix flavor. To tell one from the other, the postfix variants take an additional dummy int argument. If you overload increment or decrement, be sure to always implement both prefix and postfix versions. Here is the canonical implementation of increment, decrement follows the same rules:

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

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

    Note that the postfix variant is implemented in terms of prefix. Also note that postfix does an extra copy.2

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

    Overloading unary minus and plus is not very common and probably best avoided. If needed, they should probably be overloaded as member functions.

    2 还要注意,后缀变体做的工作更多,因此使用效率低于前缀变体.这是一个很好的理由,通常更喜欢前缀增量而不是后缀增量.虽然编译器通常可以优化内置类型的后缀增量的额外工作,但它们可能无法为用户定义的类型做同样的事情(这可能看起来像列表迭代器一样无辜).一旦你习惯了使用 i++,当 i 不是内置的时,就很难记住使用 ++i 了类型(另外你必须在更改类型时更改代码),所以最好养成始终使用前缀增量的习惯,除非明确需要后缀.

    2 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.

    对于二元算术运算符,不要忘记遵守运算符重载的第三条基本规则:如果提供+,也提供+=,如果提供-,不要省略 -= 等.据说 Andrew Koenig 是第一个观察到复合赋值运算符可以用作非复合赋值运算符的基础.即操作符++=实现,--=等实现.

    For the binary arithmetic operators, do not forget to obey the third basic rule operator overloading: If you provide +, also provide +=, if you provide -, do not omit -=, etc. Andrew Koenig is said to have been the first to observe that the compound assignment operators can be used as a base for their non-compound counterparts. That is, operator + is implemented in terms of +=, - is implemented in terms of -= etc.

    根据我们的经验法则,+ 及其同伴应该是非成员,而它们的复合赋值对应物(+= 等),改变了它们的左参数,应该是会员.这是 +=+ 的示例代码;其他二元算术运算符应该以相同的方式实现:

    According to our rules of thumb, + and its companions should be non-members, while their compound assignment counterparts (+= etc.), changing their left argument, should be a member. Here is the exemplary code for += and +; the other binary arithmetic operators should be implemented in the same way:

    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= 在每个副本中获取其参数的原因相同.

    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.3 Also note that operator+ takes its left operand by copy rather than by const reference. The reason for this is the same as the reason giving for operator= taking its argument per copy.

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

    The bit manipulation operators ~ & | ^ << >> should be implemented in the same way as the arithmetic operators. However, (except for overloading << and >> for output and input) there are very few reasonable use cases for overloading these.

    3 同样,从中吸取的教训是 a += b 通常比 a + b 更有效,如果可能的话应该是首选.

    3 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.

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

    The array subscript operator is a binary operator which must be implemented as a class member. It is used for container-like types that allow access to their data elements by a key. The canonical form of providing these is this:

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

    除非您不希望您的类的用户能够更改 operator[] 返回的数据元素(在这种情况下,您可以省略非常量变体),您应该始终提供两者运算符的变体.

    Unless you do not want users of your class to be able to change data elements returned by operator[] (in which case you can omit the non-const variant), you should always provide both variants of the operator.

    如果已知 value_type 引用内置类型,则运算符的 const 变体应该更好地返回副本而不是 const 引用:

    If value_type is known to refer to a built-in type, the const variant of the operator should better return a copy instead of a const reference:

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

    类指针类型的运算符

    要定义自己的迭代器或智能指针,你必须重载一元前缀解引用运算符*和二进制中缀指针成员访问运算符->:

    Operators for Pointer-like Types

    For defining your own iterators or smart pointers, you have to overload the unary prefix dereference operator * and the binary infix pointer member access operator ->:

    class my_ptr {
            value_type& operator*();
      const value_type& operator*() const;
            value_type* operator->();
      const value_type* operator->() const;
    };
    

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

    Note that these, too, will almost always need both a const and a non-const version. For the -> operator, if value_type is of class (or struct or union) type, another operator->() is called recursively, until an operator->() returns a value of non-class type.

    一元地址操作符永远不应该被重载.

    The unary address-of operator should never be overloaded.

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

    For operator->*() see this question. It's rarely used and thus rarely ever overloaded. In fact, even iterators do not overload it.

    继续转换运算符

相关文章