运算符重载的基本规则和习惯用法是什么? [英] What are the basic rules and idioms for operator overloading?

查看:22
本文介绍了运算符重载的基本规则和习惯用法是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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 FAQ 中说到,所以我会在这里跳过大部分内容,只列出完美的赋值运算符以供参考:

    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 继承的位操作函数,但已经变得更多在大多数应用程序中作为重载的流输入和输出运算符普遍存在.有关作为位操作运算符重载的指导,请参阅下面有关二进制算术运算符的部分.要在您的对象与 iostreams 一起使用时实现您自己的自定义格式和解析逻辑,请继续.

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

    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+ 取其左操作数通过复制 而不是通过常量引用.其原因与 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;
    };
    

    请注意,这些也几乎总是需要常量和非常量版本.对于 -> 运算符,如果 value_type 属于 class(或 structunioncode>) 类型,另一个 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.

    继续转换运算符

    这篇关于运算符重载的基本规则和习惯用法是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆