运算符重载 miniWiki

运算符概述

运算符 (operator) 是一种特殊的函数:函数名总是以 operator 为前缀,以类型名或某种特殊符号(如 +, ++, +=)为后缀。 运算符可以是普通函数,也可以是类的方法成员(隐式地以 this 为第 0 个形参)。

运算符的种类和优先级都是由语言规范所确定的。 程序员只能对已有的运算符进行重载 (overload),而不能创造新的运算符;在重载时,只能修改形参类型,而不能改变形参个数。

下面以 Point 为例,为其重载运算符:

// point.h
#include <iostream>
class Point {
  friend std::istream& operator>>(std::istream& is, Point& point);
 public:
  explicit Point(double x = 0.0, double y = 0.0)
      : x_(x), y_(y) {
  }
  // 赋值运算符
  Point& operator=(double const (array&) [2]);
  // 复合赋值运算符
  Point& operator+=(const Point& rhs);
  // 下标运算符
        double& operator[](int i);
  const double& operator[](int i) const;
  // 类型转换运算符
  explicit operator bool() const;
  // 普通方法
  const double& x() const {
    return x_;
  }
  const double& y() const {
    return y_;
  }
 private:
  double x_;
  double y_;
  static constexpr Point kOrigin;
};
// 读写运算符
std::ostream& operator<<(std::ostream& os, const Point& point);
std::istream& operator>>(std::istream& is,       Point& point);
// 算术运算符
Point operator+(const Point& lhs, const Point& rhs);
// 关系运算符
bool operator==(const Point& lhs, const Point& rhs);
bool operator!=(const Point& lhs, const Point& rhs);
bool operator< (const Point& lhs, const Point& rhs);
// point.cpp
#include "point.h"
constexpr Point Point::kOrigin;

必须重载为普通函数

读写运算符

读写 (IO) 运算符通常需要访问私有成员,因此通常需要声明为 friend

输出运算符 << 应当尽量减少对输出格式的修改(例如:不应添加换行符):

// point.cpp
#include "point.h"
#include <iostream>
std::ostream& operator<<(std::ostream& os, const Point& point) {
  os << '(' << point.x() << ", " << point.y() << ')';
  return os;
}

输入运算符 >> 必须处理(可能发生的)输入失败的情形:

// point.cpp
#include "point.h"
#include <iostream>
std::istream& operator>>(std::istream& is, Point& point) {
  is >> point.x_ >> point.y_;
  if (!is)
    point = Point();  // 输入失败,恢复到默认状态
  return is;
}

通常重载为普通函数

对称的 (symmetric) 二元运算符通常重载为普通函数。

算术运算符

算术 (arithmetic) 运算符通常返回一个新的对象(或代理)。

对于定义了复合赋值运算符的类,应当将算术运算委托给复合赋值运算符,这样可以避免将非成员的算术运算符声明为 friend

// point.cpp
#include "point.h"
Point operator+(const Point& lhs, const Point& rhs) {
  Point sum = lhs;
  sum += rhs;  // 调用 Point& Point::operator+=(const Point& rhs);
  return sum;
}

关系运算符

关系 (relational) 运算符总是返回 bool 值。

== 关系应当是传递的 (transitive),即 a == b && b == c 意味着 a == c。 类似的,<<=>>= 也应当是传递的

如果定义了 ==,则通常也应该定义 !=,并且将实现委托给前者。

< 关系应当定义出一个严格弱序 (strict weak order),并且与 ==!= 兼容,即:a != b 意味着 a < b || b < a

// point.cpp
#include "point.h"
bool operator==(const Point& lhs, const Point& rhs) {
  return lhs.x() == rhs.x() && lhs.y() == rhs.y();
}
bool operator!=(const Point& lhs, const Point& rhs) {
  return !(lhs == rhs);
}
bool operator<(const Point& lhs, const Point& rhs) {
  return lhs.x() < rhs.x() || lhs.x() == rhs.x() && lhs.y() < rhs.y();
}

位运算符

必须重载为方法成员

赋值运算符

赋值 (assignment) 运算符应当返回对左端项 (Left Hand Side, LHS) 的引用。

// point.cpp
#include "point.h"
Point& Point::operator=(double const (array&) [2]) {
  x_ = array[0];
  y_ = array[1];
  return *this;
}
// client.cpp
#include "point.h"
auto point = Point(1.0, 2.0);
double array[2] = { 0.1, 0.2 };
point = array;

下标运算符

下标 (subscript) 运算符通常应定义两个版本:

  • 普通成员函数:只能用于 non-const 对象,返回对内部数据的 non-const 引用
  • const 成员函数:可以用于任何对象,返回对内部数据的 const 引用
#include <cassert>
double& Point::operator[](int i) {
  if (i == 0)
    return x();
  if (i == 1)
    return y();
  assert(false);
}
const double& Point::operator[](int i) const {
  if (i == 0)
    return x();
  if (i == 1)
    return y();
  assert(false);
}

函数调用运算符

函数调用 (function call) 运算符可以在同一个类中重载多次,相互之间以形参类型形参数量来区分。 支持 operator() 的对象被称为函数对象,其行为可以通过设置其内部状态 (state) 的方式来订制 (customize)

// point.h
class PointBuilder {
 public:
  PointBuilder(double x, double y)
      : base_point_(x, y) {
  }
  Point operator()(double x, double y) {
    return Point(x + base_point_.x(), y + base_point_.y());
  }
 private:
  Point base_point_;
}
// client.cpp
#include "point.h"
auto builder = PointBuilder(4.0, 3.0);
auto point = builder(-4.0, -3.0);
assert(point.x() == 0.0);
assert(point.y() == 0.0);

成员访问运算符

成员访问 (member access) 运算符operator-> 通常与解引用运算符 operator* 成对地重载,用于模拟指针的行为。

operator-> 的返回类型,可以是一个指针,或者是一个支持 operator-> 的对象(例如:迭代器)。

// point.h
class PointHandle {
 public:
  PointHandle(double x = 0.0, double y = 0.0)
      : point_(new Point(x, y)) {
  }
  ~PointHandle() {
    delete point_;
  }
  Point* operator->() const {
    return point_;
  }
  Point& operator*() const {
    return *point_;
  }
 private:
  Point* point_;
};
// client.cpp
#include "point.h"
auto point_handle = PointHandle(1.0, 2.0);
assert(point_handle->x() == (*point_handle)[0]);

类型转换运算符 ⚠️

类型转换 (type cast) 运算符的函数名为 operator TargetType,形参列表为空,没有返回类型,通常应为 const 成员函数

// point.cpp
#include "point.h"
Point::operator bool() const {
  return x() != 0.0 || y() != 0.0;
}

与只需要一个实参的 non-explicit 构造函数类似,non-explicit 类型转换运算符可能隐式地用于类型转换,从而绕开类型检查机制。 ⚠️ 应当尽量避免隐式类型转换。

explicit 类型转换运算符只能显式地调用, 唯一的例外是 operator bool(),即使被声明为 explicit 也可以被隐式地调用:

// client.cpp
auto origin = Point();
assert(origin == false);
auto point = Point(1.0, 2.0);
assert(point == true);

⚠️ 除 explicit operator bool() 之外,应当避免重载类型转换运算符。

通常重载为方法成员

复合赋值运算符

Point& Point::operator+=(const Point& that) {
  this->x_ += that.x();
  this->y_ += that.y();
}

自增自减运算符

如果要自增运算符 operator++(或自减运算符 operator--),通常定义两个版本:

  • 前置 (prefix) 版本:返回新值的引用。
  • 后置 (suffix) 版本:返回旧值的副本(不含引用)。
// point.h
class PointIterator {
 public:
  Point& operator++();    // 前置版本
  Point operator++(int);  // 后置版本,其中 int 为占位符
};

解引用运算符

解引用 (dereference) 运算符 operator* 通常与成员访问运算符 operator-> 成对地重载,用于模拟指针的行为,其返回类型必须是一个引用。