gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-01 16:04:34 25 4
gpt4 key购买 nike

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

  • 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 的一个条目。如果您想批评以这种形式提供常见问题解答的想法,那么 the posting on meta that started all this 将是这样做的地方。该问题的答案在 C++ chatroom ,FAQ 的想法首先从这里开始,因此您的答案很可能会被提出该想法的人阅读。)

    最佳答案

    常用运算符重载

    重载运算符的大部分工作是样板代码。这并不奇怪,因为操作符只是语法糖,它们的实际工作可以由(并且经常被转发到)普通函数来完成。但重要的是你要正确地获得这个样板代码。如果你失败了,要么你的运算符(operator)的代码不能编译,要么你的用户的代码不能编译,要么你的用户的代码表现得令人惊讶。

    赋值运算符

    关于分配有很多话要说。然而,大部分内容已经在GMan's famous Copy-And-Swap FAQ中说到了。 ,所以我会在这里跳过大部分内容,只列出完美的赋值运算符以供引用:

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

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

    位移运算符 <<>> ,虽然仍然用于硬件接口(interface)以实现它们从 C 继承的位操作函数,但在大多数应用程序中作为重载的流输入和输出运算符变得更加普遍。有关作为位操作运算符重载的指导,请参阅下面有关二进制算术运算符的部分。当您的对象与 iostreams 一起使用时,要实现您自己的自定义格式和解析逻辑,请继续。

    流操作符是最常见的重载操作符之一,是二元中缀操作符,其语法对它们是成员还是非成员没有限制。
    由于它们改变了左参数(它们改变了流的状态),根据经验法则,它们应该被实现为它们的左操作数类型的成员。然而,它们的左操作数是来自标准库的流,虽然标准库定义的大多数流输出和输入操作符确实被定义为流类的成员,当你为自己的类型实现输出和输入操作时,你无法更改标准库的流类型。这就是为什么你需要为你自己的类型实现这些运算符作为非成员函数。
    两者的规范形式是:
    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>> ,只有在读取本身成功时才需要手动设置流的状态,但结果不是预期的。

    函数调用运算符

    函数调用运算符,用于创建函数对象,也称为仿函数,必须定义为 成员(member)函数,所以它总是隐含的 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< ,请务必遵循运算符重载的第三条基本规则,并定义所有其他 bool 比较运算符。实现它们的规范方法是这样的:
    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);}

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

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

    1 与所有经验法则一样,有时也可能有理由打破这一法则。如果是这样,不要忘记二元比较运算符的左侧操作数,对于成员函数来说,它是 *this , 需要是 const , 也。因此,作为成员函数实现的比较运算符必须具有以下签名:
    bool operator<(const X& rhs) const { /* do actual comparison with *this */ }

    (注意末尾的 const。)

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

    算术运算符

    一元算术运算符

    一元递增和递减运算符有前缀和后缀两种形式。为了区分一个和另一个,后缀变体需要一个额外的虚拟 int 参数。如果您重载 increment 或 decrement,请确保始终实现前缀和后缀版本。
    这是 increment 的规范实现,decrement 遵循相同的规则:
    class X {
    X& operator++()
    {
    // do actual increment
    return *this;
    }
    X operator++(int)
    {
    X tmp(*this);
    operator++();
    return tmp;
    }
    };

    请注意,后缀变体是根据前缀实现的。还要注意 postfix 做了一个额外的 copy.2

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

    2 另请注意,后缀变体做了更多工作,因此使用效率低于前缀变体。这是通常更喜欢前缀增量而不是后缀增量的一个很好的理由。虽然编译器通常可以优化内置类型的后缀增量的额外工作,但他们可能无法为用户定义的类型做同样的事情(这可能就像列表迭代器一样无辜)。一旦你习惯了 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+取其左操作数 通过复制 而不是通过常量引用。其原因与 operator= 的原因相同。每个拷贝取其参数。

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

    3 同样,从中吸取的教训是 a += b一般来说,比 a + b 更有效如果可能,应该首选。

    数组下标

    数组下标运算符是一个二元运算符,必​​须作为类成员实现。它用于允许通过键访问其数据元素的类容器类型。
    提供这些的规范形式是这样的:
    class X {
    value_type& operator[](index_type idx);
    const value_type& operator[](index_type idx) const;
    // ...
    };

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

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

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

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

    对于 operator->*()this question .它很少使用,因此很少重载。事实上,即使是迭代器也不会重载它。

    继续 Conversion Operators

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

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com