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

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

(注意:这是 Stack Overflow的C ++常见问题解答的条目。如果您想批评以这种形式提供FAQ的想法,请开始所有这些操作的元信息上的发布将是执行此操作的位置。 C ++聊天室,它是FAQ想法首先出现的地方,因此,提出这个想法的人很可能会读懂您的答案。)

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

回答1:

常用运算符重载

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

工作人员

关于任务有很多要说的。但是,大多数内容已经在 GMan著名的复制和交换常见问题解答中进行了介绍,因此我将在此处跳过大部分内容,仅列出完美的赋值运算符以供参考:

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

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

在实现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<,请务必遵循运算符的第三条基本规则重载,并定义所有其他布尔比较运算符。实施它们的规范方法是:

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

算术运算符

一元算术运算符

一元递增和递减运算符采用前缀和后缀形式。为了彼此区分,后缀变体采用了附加的哑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->*()的信息,请参见此问题。它很少使用,因此也很少过载。实际上,即使迭代器也不会使它过载。


继续转化运算符

回答2:

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

关于C ++中的运算符重载,有 您应遵循的三个基本规则 。与所有此类规则一样,确实存在例外。有时人们偏离了他们,结果不是不好的代码,但是这种积极的偏差很少而且相差甚远。至少在我看到的100个此类偏差中,有99个是不合理的。但是,也可能是千分之九千。因此,您最好遵循以下规则。

  1. 只要运算符的含义不明显且无争议,就不应重载。 相反,请提供带有修饰符的函数选择的名称。
    基本上,重载运算符的首要原则就是说:不要这样做。这可能看起来很奇怪,因为关于运算符重载有很多已知的知识,因此许多文章,书籍章节和其他文本都涉及到这一切。尽管有这些看似显而易见的证据,但只有极少数情况下适合使用运算符重载。原因是实际上很难理解运算符应用程序背后的语义,除非在应用程序域中对运算符的使用是众所周知且无可争议的。与普遍的看法相反,几乎没有这种情况。

  2. 始终坚持运算符的众所周知的语义。
    C ++对重载运算符的语义没有任何限制。您的编译器将很乐意接受实现二进制+运算符的代码,以从其右操作数中减去。但是,这种运算符的用户永远不会怀疑表达式a+bb中减去a。当然,这假定在应用程序域中运算符的语义是无可争议的。

  3. 总是提供一组相关操作中的所有内容。
    操作员彼此关联操作。如果您的类型支持a+b,则用户也希望能够调用a+=b。如果它支持前缀增量++a,则他们希望a++也能正常工作。如果他们可以检查a,那么他们肯定会希望也能够检查a>b。如果他们可以复制构造您的类型,则希望分配也能正常工作。


继续成员与非成员之间的决定

>

回答3:

C ++中运算符重载的一般语法

您不能更改C ++中内置类型的运算符的含义,只能对用户定义的类型 1 重载运算符。即,至少一个操作数必须是用户定义的类型。与其他重载函数一样,运算符只能对一组特定参数重载一次。

并非所有运算符都可以在C ++中重载。不能重载的运算符包括:. :: sizeof typeid .*和C ++中唯一的三元运算符?:

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

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

但是,您可以使所有这些过载的事实并不意味着您应该这样做。请参阅运算符重载的基本规则。

在C ++中,运算符以具有特殊名称的 函数 的形式重载。与其他函数一样,重载运算符通常可以实现为其左操作数类型的 成员函数 ,也可以实现为 非成员函数 > 。是否自由选择或绑定使用这两个条件取决于几个标准。 2 一元运算符@ 3 应用于对象x ,作为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元运算符必须始终作为成员函数实现。


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

回答4:

成员与非成员之间的决定

二进制运算符=(分配),[](数组订阅),->(成员访问权)以及n元()(函数调用)运算符必须始终作为 成员函数 实现,因为语言的语法要求它们。

其他运算符可以实现为成员或非成员。但是,其中一些通常必须实现为非成员函数,因为您无法修改其左操作数。其中最突出的是输入和输出运算符<<>> ,其左操作数是来自标准库的流类,您不能更改。

对于必须选择将其实现为成员函数或非成员函数的所有运算符, 使用以下经验法则 确定:< / p>

  1. 如果它是 一元运算符 ,请将其实现为 成员 功能。
  2. 如果二元运算符将 两个操作数均等对待 (使其保持不变),则将该运算符实现为 非成员 功能。
  3. 如果二元运算符 对两个操作数进行 相等 处理(通常会更改其左键操作数),如果它必须访问操作数的私有部分,则使其成为其左侧操作数类型的 成员 函数可能很有用。

当然,与所有经验法则一样,也有例外。如果您输入的是

enum Month {Jan, Feb, ..., Nov, Dec}

,并且您想为其重载增量和减量运算符,因此不能作为成员函数来执行此操作,因为在C ++中,枚举类型不能具有成员函数。因此,您必须将其作为自由函数进行重载。当嵌套在类定义中作为成员函数完成时,嵌套在类模板中的类模板的operator<()更加容易编写和读取。但是这些确实是罕见的例外。

(但是,如果是例外,请不要忘记操作数的const -ness问题,对于成员函数,该操作数成为隐式如果不是非成员函数的运算符将其最左边的参数作为const引用,则与成员函数相同的运算符需要具有const作为结尾,将*this用作const引用。)


继续常用运算符重载

回答5:

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

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

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

隐式转换运算符允许编译器将用户定义类型的值隐式转换(例如,在intlong之间进行转换)。 / p>

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

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

隐式转换运算符(如一参数构造函数)是用户定义的转换。尝试将调用与重载函数匹配时,编译器将授予一个用户定义的转换。

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

起初这似乎很有帮助,但是问题在于隐式转换甚至会在预期不到的时候启动。在以下代码中,将调用voidf(constchar*),因为my_string()不是 lvalue ,因此第一个不匹配:

void f(my_string&);
void f(const char*);

f(my_string());

初学者容易犯错,甚至经验丰富的C ++程序员有时也会感到惊讶,因为编译器会选择他们不怀疑的重载。这些问题可以通过显式转换运算符来缓解。

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

与隐式转换运算符不同,显式转换运算符在您不希望它们出现时永远不会起作用。以下是带有显式转换运算符的简单类:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

注意显式。现在,当您尝试从隐式转换运算符执行意外代码时,会出现编译器错误:

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_cast,C样式强制转换或构造函数样式强制转换(即T(value))。

但是,有一个例外:允许编译器隐式转换为bool。此外,编译器在转换为bool后,不允许进行其他隐式转换(编译器一次只能进行2次隐式转换,但最多只能进行1个用户定义的转换)。

由于编译器不会强制转换"过去" bool,因此显式转换运算符现在不再需要安全布尔成语。例如,C ++ 11之前的智能指针使用Safe Bool习惯用法来防止转换为整数类型。在C ++ 11中,智能指针使用显式运算符代替,因为在将类型显式转换为bool后,不允许编译器隐式转换为整数。

继续重载newdelete

回答6:

重载newdelete

注意: :这仅处理new重载的语法.code>和delete,而不是此类重载运算符的 实现 。我认为 newdelete重载的语义值得他们自己的常见问题解答 ,在操作符重载的主题中,我永远无法做到这一点。

基础

在C ++中,当您编写 新表达式 时,例如newT(arg),在计算该表达式时会发生两件事:首先<调用strong> operatornew 以获得原始内存,然后调用 T的适当构造函数以转换此原始内存变成一个有效的对象。同样,删除对象时,首先调用其析构函数,然后将内存返回给 operatordelete
C ++允许您调整以下两个操作:内存管理和构造/在分配的内存中销毁对象。后者是通过为类编写构造函数和析构函数来完成的。通过编写自己的 operatornewoperatordelete,可以对内存管理进行微调。

运算符重载的第一个基本规则-不这样做 –特别适用于重载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(); 

前两个为对象分配/释放内存,后两个为对象数组分配/释放内存。如果您提供自己的版本,它们将 不会重载,而是替换 标准库中的版本。
如果您重载operatornew,即使您从未打算调用它,也应始终使匹配的operatordelete重载。原因是,如果构造函数在对新表达式求值时抛出异常,则运行时系统会将内存返回给与operatornew相匹配的operatordelete。调用以分配用于创建对象的内存。如果没有提供匹配的operatordelete,则将调用默认值,这几乎总是错误的。
如果重载了newdelete,您也应该考虑重载数组变量。

展示位置

C ++允许new和delete运算符接受其他参数。
所谓的place new允许您在某个地址处创建对象,该对象将传递给:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

标准库为此提供了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(); 

请注意,在上面给出的用于放置新位置的示例代码中,除非X的构造函数抛出异常,否则永远不会调用operatordelete

您还可以使用其他参数重载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);
    // ... 
}; 

因此,重载后,new和delete的行为类似于静态成员函数。对于my_class的对象,std::size_t参数将始终为sizeof(my_class)。但是,还需要为这些 派生类 动态分配的对象调用这些运算符,在这种情况下,它可能会更大。

全局新建和删除

要使全局new和delete重载,只需用我们自己的标准库替换预定义的运算符。但是,很少需要这样做。

回答7:

为何operator<<功能不能用于流式传输对象到std::cout还是文件成为成员函数?

假设您拥有:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

鉴于此,您不能使用:

Foo f = {10, 20.0};
std::cout << f;

由于operator<<作为Foo的成员函数而被重载,因此该运算符的LHS必须是Foo对象。也就是说,您将需要使用:

Foo f = {10, 20.0};
f << std::cout

这很不直观。

如果您将其定义为非成员函数,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

您将可以使用:

Foo f = {10, 20.0};
std::cout << f;

非常直观。

回到顶部