2338

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

(注意:这是对Stack Overflow 的 C++ FAQ 的一个条目。如果您想批评以这种形式提供 FAQ 的想法,那么开始这一切的 meta 上的帖子就是这样做的地方。该问题在C++ 聊天室中进行监控,FAQ 想法最初是从那里开始的,因此您的答案很可能会被提出该想法的人阅读。)

4

7 回答 7

1139

常用运算符重载

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

赋值运算符

关于任务有很多话要说。不过大部分已经在GMan 著名的 Copy-And-Swap FAQ中讲过了,这里就跳过大部分,只列出完美的赋值运算符供参考:

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

位移运算符(用于流 I/O)

移位运算符<<>>,虽然仍然用于从 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>>时,只有在读取本身成功时才需要手动设置流的状态,但结果不是预期的。

函数调用运算符

用于创建函数对象的函数调用运算符,也称为函子,必须定义为成员函数,因此它始终具有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;
  }
};

请注意,后缀变体是根据前缀实现的。另请注意,postfix 做了一个额外的副本。2

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

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

二元算术运算符

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

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

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 另请注意,通过复制而不是通过 const 引用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;
};

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

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

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


继续转换运算符

于 2010-12-12T12:47:05.033 回答
531

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

当谈到 C++ 中的运算符重载时,您应该遵循三个基本规则。与所有此类规则一样,确实有例外。有时人们已经偏离了它们,结果不是糟糕的代码,但这种积极的偏差很少而且相差甚远。至少,我见过的 100 个这样的偏差中有 99 个是不合理的。但是,它也可能是 1000 中的 999。因此,您最好遵守以下规则。

  1. 每当一个运算符的含义不是很清楚和无可争议的时候,它就不应该被重载。 相反,提供一个名称选择得当的函数。
    基本上,重载运算符的首要规则,其核心是:不要这样做。这可能看起来很奇怪,因为有很多关于运算符重载的知识,所以很多文章、书籍章节和其他文本都涉及到这一切。但是尽管有这些看似明显的证据,但只有极少数情况下运算符重载是合适的. 原因是实际上很难理解运算符应用背后的语义,除非运算符在应用领域的使用是众所周知且无可争议的。与普遍的看法相反,这种情况几乎从未发生过。

  2. 始终坚持运营商众所周知的语义。
    C++ 对重载运算符的语义没有任何限制。您的编译器将愉快地接受实现二元+运算符的代码以从其右操作数中减去。但是,这种运算符的用户永远不会怀疑要从a + b中减去。当然,这是假设算子在应用领域的语义是无可争议的。ab

  3. 始终提供一组相关操作中的所有操作。
    运算符相互关联并与其他操作相关。如果您的类型支持a + b,用户也将期望能够调用a += b。如果它支持前缀增量++a,他们也将期望a++工作。如果他们能检查是否a < b,他们肯定会期望也能检查是否a > b。如果他们可以复制构造您的类型,他们希望分配也能工作。


继续会员和非会员之间的决定

于 2010-12-12T12:45:14.160 回答
279

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

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

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

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

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

然而,你可以超载所有这些并不意味着你应该这样做。请参阅运算符重载的基本规则。

在 C++ 中,运算符以具有特殊名称的函数的形式重载。与其他函数一样,重载运算符通常可以实现为左操作数类型的成员函数或非成员函数。您是否可以自由选择或一定要使用其中任何一个取决于几个标准。2应用于对象 x的一元运算符@3operator@(x)被调用为 as或 as x.operator@()@应用于对象xand的二元中缀运算符y称为 asoperator@(x,y)或 as x.operator@(y)4

作为非成员函数实现的运算符有时是其操作数类型的朋友。

1 “用户定义”一词可能有点误导。C++ 区分了内置类型和用户定义类型。前者属于例如 int、char 和 double;后者属于所有 struct、class、union 和 enum 类型,包括标准库中的那些,即使它们不是由用户定义的。

2 这将在本常见问题解答的后面部分中介绍。

3 The @is not a valid operator in C++ 这就是我将它用作占位符的原因。

4 C++ 中唯一的三元运算符不能重载,唯一的 n 元运算符必须始终作为成员函数实现。


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

于 2010-12-12T12:46:30.573 回答
275

会员与非会员之间的决定

二元运算符=(赋值)、[](数组订阅)、->(成员访问)以及 n 元()(函数调用)运算符必须始终实现为成员函数,因为语言的语法要求它们这样做。

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

对于必须选择将它们实现为成员函数或非成员函数的所有运算符,请使用以下经验法则来决定:

  1. 如果是一元运算符,则将其实现为成员函数。
  2. 如果二元运算符同等对待两个操作数(保持不变),则将此运算符实现为非成员函数。
  3. 如果一个二元运算符没有平等对待它的两个操作数(通常它会改变它的左操作数) ,如果它必须访问操作数的私有部分,让它成为它的左操作数类型的成员函数可能会很有用。

当然,正如所有经验法则一样,也有例外。如果你有一个类型

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

并且您想为其重载递增和递减运算符,您不能将其作为成员函数执行,因为在 C++ 中,枚举类型不能具有成员函数。所以你必须将它作为一个自由函数重载。operator<()对于嵌套在类模板中的类模板,当作为类定义中的内联成员函数完成时,更容易编写和阅读。但这些确实是罕见的例外。

(但是,如果你有一个例外,不要忘记操作数的const-ness 问题,对于成员函数,它成为隐式this参数。如果运算符作为非成员函数将其最左边的参数作为const引用, 与成员函数相同的运算符需要const在末尾有a才能进行*this引用const。)


继续对Common 运算符进行重载

于 2010-12-12T12:49:13.380 回答
175

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

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

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

隐式转换运算符允许编译器将用户定义类型的值隐式转换(如和之间的转换intlong到其他类型。

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

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*() )

起初这似乎很有帮助,但这样做的问题是隐式转换甚至在不期望的时候启动。在下面的代码中,void f(const char*)会被调用,因为my_string()不是左值,所以第一个不匹配:

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

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

prog.cpp:在函数'int main()'中:
prog.cpp:15:18: 错误: 没有匹配函数调用'f(my_string)'
prog.cpp:15:18: 注意:候选人是:
prog.cpp:11:10: 注意: void f(my_string&)
prog.cpp:11:10:注意:没有已知的参数 1 从“my_string”到“my_string&”的转换
prog.cpp:12:10: 注意: void f(const char*)
prog.cpp:12:10:注意:没有已知的参数 1 从“my_string”到“const char*”的转换

要调用显式转换运算符,您必须使用static_castC 样式转换或构造函数样式转换 ( ie T(value))。

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

因为编译器不会强制转换“过去” bool,所以显式转换运算符现在不再需要Safe Bool idiom。例如,C++11 之前的智能指针使用 Safe Bool 习惯用法来防止转换为整数类型。在 C++11 中,智能指针使用显式运算符,因为编译器在将类型显式转换为 bool 后不允许隐式转换为整数类型。

继续重载newdelete

于 2013-05-17T18:32:00.563 回答
158

Overloading new and delete operators

Note: This only deals with the syntax of overloading new and delete, not with the implementation of such overloaded operators. I think that the semantics of overloading new and delete deserve their own FAQ, within the topic of operator overloading I can never do it justice.

Basics

In C++, when you write a new expression like new T(arg) two things happen when this expression is evaluated: First operator new is invoked to obtain raw memory, and then the appropriate constructor of T is invoked to turn this raw memory into a valid object. Likewise, when you delete an object, first its destructor is called, and then the memory is returned to operator delete.
C++ allows you to tune both of these operations: memory management and the construction/destruction of the object at the allocated memory. The latter is done by writing constructors and destructors for a class. Fine-tuning memory management is done by writing your own operator new and operator delete.

The first of the basic rules of operator overloading – don’t do it – applies especially to overloading new and delete. Almost the only reasons to overload these operators are performance problems and memory constraints, and in many cases, other actions, like changes to the algorithms used, will provide a much higher cost/gain ratio than attempting to tweak memory management.

The C++ standard library comes with a set of predefined new and delete operators. The most important ones are these:

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

The first two allocate/deallocate memory for an object, the latter two for an array of objects. If you provide your own versions of these, they will not overload, but replace the ones from the standard library.
If you overload operator new, you should always also overload the matching operator delete, even if you never intend to call it. The reason is that, if a constructor throws during the evaluation of a new expression, the run-time system will return the memory to the operator delete matching the operator new that was called to allocate the memory to create the object in. If you do not provide a matching operator delete, the default one is called, which is almost always wrong.
If you overload new and delete, you should consider overloading the array variants, too.

Placement new

C++ allows new and delete operators to take additional arguments.
So-called placement new allows you to create an object at a certain address which is passed to:

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

The standard library comes with the appropriate overloads of the new and delete operators for this:

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

Note that, in the example code for placement new given above, operator delete is never called, unless the constructor of X throws an exception.

You can also overload new and delete with other arguments. As with the additional argument for placement new, these arguments are also listed within parentheses after the keyword new. Merely for historical reasons, such variants are often also called placement new, even if their arguments are not for placing an object at a specific address.

Class-specific new and delete

Most commonly you will want to fine-tune memory management because measurement has shown that instances of a specific class, or of a group of related classes, are created and destroyed often and that the default memory management of the run-time system, tuned for general performance, deals inefficiently in this specific case. To improve this, you can overload new and delete for a specific class:

class my_class { 
  public: 
    // ... 
    void* operator new(std::size_t);
    void  operator delete(void*);
    void* operator new[](std::size_t);
    void  operator delete[](void*);
    // ...  
}; 

Overloaded thus, new and delete behave like static member functions. For objects of my_class, the std::size_t argument will always be sizeof(my_class). However, these operators are also called for dynamically allocated objects of derived classes, in which case it might be greater than that.

Global new and delete

To overload the global new and delete, simply replace the pre-defined operators of the standard library with our own. However, this rarely ever needs to be done.

于 2010-12-12T13:07:17.567 回答
50

为什么将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;

这是非常直观的。

于 2016-01-22T19:00:54.003 回答