运算符重载

来自cppreference.com
< cpp‎ | language
 
 
 
表达式
概述
值类别(左值 lvalue、右值 rvalue、亡值 xvalue)
求值顺序(序列点)
常量表达式
潜在求值表达式
初等表达式
lambda 表达式(C++11)
字面量
整数字面量
浮点字面量
布尔字面量
字符字面量,包含转义序列
字符串字面量
空指针字面量(C++11)
用户定义字面量(C++11)
运算符
赋值运算符a=ba+=ba-=ba*=ba/=ba%=ba&=ba|=ba^=ba<<=ba>>=b
自增与自减++a--aa++a--
算术运算符+a-aa+ba-ba*ba/ba%b~aa&ba|ba^ba<<ba>>b
逻辑运算符a||ba&&b!a
比较运算符a==ba!=ba<ba>ba<=ba>=ba<=>b(C++20)
成员访问运算符a[b]*a&aa->ba.ba->*ba.*b
其他运算符a(...)a,ba?b:c
new 表达式
delete 表达式
throw 表达式
alignof
sizeof
sizeof...(C++11)
typeid
noexcept(C++11)
折叠表达式(C++17)
运算符的代用表示
优先级和结合性
运算符重载
默认比较(C++20)
类型转换
隐式转换
一般算术转换
const_cast
static_cast
reinterpret_cast
dynamic_cast
显式转换 (T)a, T(a)
用户定义转换
 

为用户定义类型的操作数定制 C++ 运算符。

语法

重载的运算符是具有特殊的函数名的函数

operator 运算符 (1)
operator 类型 (2)
operator new
operator new []
(3)
operator delete
operator delete []
(4)
operator "" 后缀标识符 (5) (C++11 起)
operator co_await (6) (C++20 起)
运算符 - 下列运算符之一:+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=> (C++20 起) && || ++ -- , ->* -> ( ) [ ]
1) 重载的运算符;
6) 用于 co_await 表达式的重载的 co_await 运算符。

重载的运算符

表达式中出现某个运算符,且它至少有-一个操作数拥有类类型枚举类型时,使用重载决议在具有与以下各项匹配的签名的函数中,确定所要调用的用户定义函数:

表达式 作为成员函数 作为非成员函数 示例
@a (a).operator@ ( ) operator@ (a) !std::cin 调用 std::cin.operator!()
a@b (a).operator@ (b) operator@ (a, b) std::cout << 42 调用 std::cout.operator<<(42)
a=b (a).operator= (b) 不能是非成员 给定 std::string s;s = "abc"; 调用 s.operator=("abc")
a(b...) (a).operator()(b...) 不能是非成员 给定 std::random_device r;auto n = r(); 调用 r.operator()()
a[b] (a).operator[](b) 不能是非成员 给定 std::map<int, int> m;m[1] = 2; 调用 m.operator[](1)
a-> (a).operator-> ( ) 不能是非成员 给定 auto p = std::make_unique<S>(); p->bar() 调用 p.operator->()
a@ (a).operator@ (0) operator@ (a, 0) 给定 std::vector<int>::iterator i = v.begin();i++ 调用 i.operator++(0)

在这张表中,@ 是表示所有匹配运算符的占位符:@a 是所有前缀运算符,a@ 是除 -> 以外的所有后缀运算符,a@b 是除 = 以外的所有其他运算符。

另外,对于比较运算符 ==, !=, <, >, <=, >=, <=> ,重载决议也会考虑从 operator==operator<=> 生成的重写候选

(C++20 起)

注意:对于重载的 co_await (C++20 起)用户定义转换函数用户定义字面量分配解分配,可分别见对应专题。

重载的运算符(但非内建运算符)可用函数记法进行调用:

std::string str = "Hello, ";
str.operator+=("world");                       // 同 str += "world";
operator<<(operator<<(std::cout, str) , '\n'); // 同 std::cout << str << '\n';
                                               // (C++17 起) 但定序不同

限制

  • 不能重载 ::(作用域解析)、.(成员访问)、.*(通过成员指针的成员访问)及 ?:(三元条件)运算符。
  • 不能创建新运算符,例如 **<>&|
  • 运算符的优先级、结合方向或操作数的数量不会变化。
  • 重载的运算符 -> 必须要么返回裸指针,要么(按引用或值)返回同样重载了运算符 -> 的对象。
  • 运算符 &&|| 的重载失去短路求值。
  • &&||,(逗号)在被重载时失去它们特殊的定序性质,并且即使不使用函数调用记法,也表现为与常规的函数调用相似。
(C++17 前)

规范实现

除了上述限制外,语言对重载运算符的所作所为或返回类型(它不参与重载决议)上没有其他任何制约。

但通常期待重载的运算符表现尽可能与内建运算符相似:

 期待 operator+ 对它的实参进行相加而非相乘,
 期待 operator= 进行赋值,如此等等。
 期待相关的运算符之间的表现也相似(operator+operator+= 做同一类加法运算)。

返回类型被期待使用该运算符的表达式限制:

 例如,令赋值运算符按引用返回,以使写出 a = b = c = d 可行,因为内建运算符允许这样做。

常见的重载运算符拥有下列典型、规范形式:[1]

赋值运算符

赋值运算符(operator=)有特殊性质:细节见复制赋值移动赋值

对规范的复制赋值运算符,期待它能安全的处理自赋值,并按引用返回左操作数:

// 复制赋值
T& operator=(const T& other)
{
    // 防止自赋值
    if (this == &other)
        return *this;
 
    // 假设 *this 保有可重用资源,例如一个在堆的缓冲区分配的 mArray
    if (size != other.size)           // *this 中的存储不可复用
    {
        temp = new int[other.size];   // 分配存储,如果抛出异常则等同于什么也不做
        delete[] mArray;              // 销毁 *this 中的存储
        mArray = temp;
        size = other.size;
    } 
 
    std::copy(other.mArray, other.mArray + other.size, mArray);
    return *this;
}

对规范的移动赋值,期待它令被移动对象遗留于合法状态(即有完好类不变式的状态),且在自赋值时要么不做任何事,要么至少遗留对象于合法状态,并以非 const 引用返回左操作数,而且是 noexcept 的:

T& operator=(T&& other) noexcept // 移动赋值
{
    // 防止自赋值
    if (this == &other)
        return *this; // delete[]/size=0 也可以
 
    delete[] mArray;                               // 释放 *this 中的资源
    mArray = std::exchange(other.mArray, nullptr); // 令 other 遗留在合法状态
    size = std::exchange(other.size, 0);
    return *this;
}
(C++11 起)

在复制赋值不能从资源复用中受益的情形下(它不管理堆分配数组,且不含这么做的(可能传递的)成员,例如 std::vectorstd::string 成员),有一种流行的便捷方式:复制并交换(copy-and-swap)赋值运算符,它按值接收形参(从而根据实参的值类别而同时支持复制和移动赋值),交换形参,并令析构函数进行清理。

// 复制赋值(复制并交换)
T& T::operator=(T other) noexcept // 调用复制/移动构造函数以构造 other
{
    std::swap(size, other.size); // 在 *this 与 other 间交换资源
    std::swap(mArray, other.mArray);
    return *this;
} // 调用 other 的析构函数以释放先前 *this 所保有的资源

这种形式自动提供强异常保证,但禁止资源复用。

流的提取与插入

接受 std::istream&std::ostream& 作为左侧实参的 operator>>operator<< 的重载,被称为插入与提取运算符。因为它们接收用户定义类型为右实参(a@b 中的 b),所以它们必须以非成员实现。

std::ostream& operator<<(std::ostream& os, const T& obj)
{
    // 向流写入 obj
    return os;
}
 
std::istream& operator>>(std::istream& is, T& obj)
{
    // 从流读取 obj
    if( /* 不能构造 T */ )
        is.setstate(std::ios::failbit);
    return is;
}

这些运算符有时实现为友元函数

函数调用运算符

当用户定义的类重载了函数调用运算符 operator() 时,它就成为函数对象 (FunctionObject) 类型。

这种类型的对象能用于函数调用式的表达式:

// 此类型的对象表示一个变量的线性函数 a * x + b 。
struct Linear
{
    double a, b;
 
    double operator()(double x) const
    {
        return a * x + b;
    }
};
 
int main()
{
    Linear f{2, 1}; // 表示函数 2x + 1 。
    Linear g{-1, 0}; // 表示函数 -x 。
    // f 和 g 是能像函数一样使用的对象。
 
    double f_0 = f(0);
    double f_1 = f(1);
 
    double g_0 = g(0);
}

std::sortstd::accumulate 的许多标准算法都接受函数对象 (FunctionObject) 以定制它们的行为。operator() 没有特别值得注意的规范形式,此处演示它的用法:

#include <algorithm>
#include <iostream>
#include <vector>
 
struct Sum
{
    int sum = 0;
    void operator()(int n) { sum += n; }
};
 
int main()
{
    std::vector<int> v = {1, 2, 3, 4, 5};
    Sum s = std::for_each(v.begin(), v.end(), Sum());
    std::cout << "和为 " << s.sum << '\n';
}

输出:

和为 15

参阅 Lambda 表达式

自增与自减

当表达式中出现后缀自增与自减时,以一个整数实参 0 调用用户定义函数(operator++operator--)。它典型地实现为 T operator++(int),其中参数被忽略。后缀自增与自减运算符通常以前缀版本实现:

struct X
{
    // 前缀自增
    X& operator++()
    {
        // 实际上的自增在此进行
        return *this; // 以引用返回新值
    }
 
    // 后缀自增
    X operator++(int)
    {
        X old = *this; // 复制旧值
        operator++();  // 前缀自增
        return old;    // 返回旧值
    }
 
    // 前缀自减
    X& operator--()
    {
        // 实际上的自减在此进行
        return *this; // 以引用返回新值
    }
 
    // 后缀自减
    X operator--(int)
    {
        X old = *this; // 复制旧值
        operator--();  // 前缀自减
        return old;    // 返回旧值
    }
};

尽管前自增/前自减的规范形式是返回引用的,但同任何运算符重载一样,它的返回类型是用户定义的;例如这些运算符对 std::atomic 的重载返回值。

二元运算符

典型情况下,二元运算符都被实现为两个类型对称的非成员以维持对称性(例如,将复数与整数相加时,如果 operator+ 是复数类型的成员函数,那么只有复数 + 整数能编译,而整数 + 复数不能)。

编译器可从单向的 operator==operator<=> 生成另一方向的比较,以维持对称性。例如若存在 X::operator==(const Y&) 则对于 X aY b,总是能比较 b == a

(C++20 起)

因为每个二元算术运算符都存在对应的复合赋值运算符,所以二元算数运算符的规范形式是基于它对应的复合赋值实现的:

class X
{
public:
    X& operator+=(const X& rhs) // 复合赋值(不必,但通常是成员函数,以修改私有成员)
    {
        /* 将 rhs 加到 *this 发生于此 */
        return *this; // 以引用返回结果
    }
 
    // 在类体内定义的友元是 inline 的,且在非 ADL 查找中被隐藏
    friend X operator+(X lhs,        // 按值传递 lhs 有助于优化链状的 a + b + c
                       const X& rhs) // 否则,两个形参都是 const 引用
    {
        lhs += rhs; // 复用复合赋值
        return lhs; // 以值返回结果(使用移动构造函数)
    }
};

比较运算符

标准(库的)算法(如 std::sort)和容器(如 std::set)在默认情况下期待 operator< 对于用户提供的类型有定义,并期待它实现严格弱序(从而满足比较 (Compare) 要求)。一种为结构体实现严格弱序的惯用方式是使用 std::tie 提供的字典序比较:

struct Record
{
    std::string name;
    unsigned int floor;
    double weight;
 
    friend bool operator<(const Record& l, const Record& r)
    {
        return std::tie(l.name, l.floor, l.weight)
             < std::tie(r.name, r.floor, r.weight); // 保持相同顺序
    }
};

典型地,一旦提供了 operator<,其他关系运算符就都能通过 operator< 来实现。

inline bool operator< (const X& lhs, const X& rhs) { /* 做实际比较 */ }
inline bool operator> (const X& lhs, const X& rhs) { return rhs < lhs; }
inline bool operator<=(const X& lhs, const X& rhs) { return !(lhs > rhs); }
inline bool operator>=(const X& lhs, const X& rhs) { return !(lhs < rhs); }

类似地,不相等运算符典型地通过 operator== 来实现:

inline bool operator==(const X& lhs, const X& rhs) { /* 做实际比较 */ }
inline bool operator!=(const X& lhs, const X& rhs) { return !(lhs == rhs); }

当提供了三路比较(如 std::memcmpstd::string::compare)时,所有六个双路比较运算符都能通过它表达:

inline bool operator==(const X& lhs, const X& rhs) { return cmp(lhs,rhs) == 0; }
inline bool operator!=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) != 0; }
inline bool operator< (const X& lhs, const X& rhs) { return cmp(lhs,rhs) <  0; }
inline bool operator> (const X& lhs, const X& rhs) { return cmp(lhs,rhs) >  0; }
inline bool operator<=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) <= 0; }
inline bool operator>=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) >= 0; }

如果定义了 operator== ,那么编译器会自动生成不等运算符。类似地,如果定义了三路比较运算符 operator<=> ,那么编译器会自动生成四个关系运算符。如果定义 operator<=> 为预置,那么编译器会生成 operator==operator<=>

struct Record
{
    std::string name;
    unsigned int floor;
    double weight;
 
    auto operator<=>(const Record&) = default;
};
// 现在能用 ==、!=、<、<=、> 和 >= 比较 Record

细节见默认比较

(C++20 起)

数组下标运算符

提供数组式访问并同时允许读写的用户定义类,典型地为 operator[] 定义两个重载:const 和非 const 变体:

struct T
{
          value_t& operator[](std::size_t idx)       { return mVector[idx]; }
    const value_t& operator[](std::size_t idx) const { return mVector[idx]; }
};

如果已知值类型是标量类型,那么 const 变体应按值返回。

当不希望或不可能直接访问容器元素,或者要区别左值(c[i] = v;)和右值(v = c[i];)的不同用法时,operator[] 可以返回代理。示例见 std::bitset::operator[]

下标运算符只能接收一个下标 (C++23 前),为提供多维数组访问语义,例如实现三维数组访问 a[i][j][k] = x;operator[] 必须返回到二维平面的引用,它必须拥有自己的 operator[] 并返回到一维行的引用,而行必须拥有返回到元素的引用的 operator[]。为避免这种复杂性,一些库选择代之以重载 operator(),使得 3D 访问表达式拥有 Fortran 式的语法 a(i, j, k) = x;

下标运算符能接收多于一个下标。例如 3D 数组类的一个声明为 T& operator[](std::size_t x, std::size_t y, std::size_t z);operator[] 能直接访问元素。

(C++23 起)
// https://godbolt.org/z/993s5dK7z
 
#include <array>
#include <cassert>
#include <iostream>
#include <numeric>
#include <tuple>
 
template<typename T, std::size_t X, std::size_t Y, std::size_t Z>
class array3d
{
    std::array<T, X * Y * Z> a;
public:
    array3d() = default;
    array3d(array3d const&) = default;
 
    constexpr T& operator[](std::size_t x, std::size_t y, std::size_t z) // C++23
    {
        assert(x < X and y < Y and z < Z);
        return a[z * Y * X + y * X + x];
    }
 
    constexpr auto& underlying_array() { return a; }
 
    constexpr std::tuple<std::size_t, std::size_t, std::size_t> xyz() const
    {
        return {X, Y, Z};
    }
};
 
int main()
{
    array3d<char, 4, 3, 2> v;
    v[3, 2, 1] = '#';
    std::cout << "v[3, 2, 1] = '" << v[3, 2, 1] << "'\n";
 
    // 填充底层一维数组
    auto& arr = v.underlying_array();
    std::iota(arr.begin(), arr.end(), 'A');
 
    // print out as 3D array using the order: X -> Z -> Y
    const auto [X, Y, Z] = v.xyz();
    for (auto y {0U}; y < Y; ++y)
    {
        for (auto z {0U}; z < Z; ++z)
        {
            for (auto x {0U}; x < X; ++x)
                std::cout << v[x, y, z] << ' ';
            std::cout << "│ ";
        }
        std::cout << '\n';
    }
}

输出:

v[3, 2, 1] = '#'
A B C D │ M N O P │ 
E F G H │ Q R S T │ 
I J K L │ U V W X │

逐位算术运算符

实现位掩码类型 (BitmaskType) 的规定的用户定义类和枚举,要求重载逐位算术运算符 operator&operator|operator^operator~operator&=operator|=operator^=,而且可重载位移运算符 operator<<operator>>operator>>=operator<<=。规范实现通常遵循上述的二元算术运算符。

布尔取反运算符

有意用于布尔语境的用户定义类常重载运算符 operator! 。这种类也会提供用户定义转换函数 explicit operator bool()(标准库样例见 std::basic_ios),而 operator! 的受期待行为是返回 operator bool 的取反。

(C++11 前)

由于内建运算符 ! 进行按语境到 bool 的转换,有意用于布尔语境的用户定义类可以只提供 operator bool 而无需重载 operator!

(C++11 起)

罕有重载的运算符

下列运算符罕有重载:

  • 取址运算符 operator&。如果对不完整类型的左值应用一元 &,而完整类型声明了重载的 operator&,那么未指明运算符拥有内建含义还是调用运算符函数。因为此运算符可能被重载,所以泛型库都用 std::addressof 取得用户定义类型的对象的地址。最为人熟知的规范重载的 operator& 是 Microsoft 类 CComPtrBase。在 boost.spirit 中可以找到该运算符在 EDSL 的使用案例。
  • 布尔逻辑运算符 operator&&operator||。不同于内建版本,重载版本无法实现短路求值。而且不同于内建版本,它们也不会令左操作数的求值按顺序早于右操作数。 (C++17 前)标准库中,这些运算符仅由 std::valarray 重载。
  • 逗号运算符 operator,不同于内建版本,重载版本不会令左操作数的求值按顺序早于右操作数。 (C++17 前)因为此运算符可能被重载,所以泛型库都用 a,void(),b 这种表达式取代 a,b,以对用户定义类型的表达式按顺序求值。boost 库在 boost.assignboost.spirit 及几个其他库中使用 operator,。数据库访问库 SOCI 也重载了 operator,
  • 通过成员指针的成员访问 operator->*。重载此运算符并没有特别缺点,但实践中少有使用。有人推荐这能作为智能指针接口的一部分,且实际上在 boost.phoenix 中的 actor 有实际用途。它在像 cpp.react 这样的 EDSL 中更常见。

注解

功能特性测试 标准 注释
__cpp_static_call_operator 202207L (C++23) static operator()
__cpp_multidimensional_subscript 202211L (C++23) static operator[]

示例

#include <iostream>
 
class Fraction
{
    // 或 C++17 的 std::gcd
    int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
 
    int n, d;
public:
    Fraction(int n, int d = 1) : n(n / gcd(n, d)), d(d / gcd(n, d)) {}
 
    int num() const { return n; }
    int den() const { return d; }
 
    Fraction& operator*=(const Fraction& rhs)
    {
        int new_n = n * rhs.n / gcd(n * rhs.n, d * rhs.d);
        d = d * rhs.d / gcd(n * rhs.n, d * rhs.d);
        n = new_n;
        return *this;
    }
};
 
std::ostream& operator<<(std::ostream& out, const Fraction& f)
{
   return out << f.num() << '/' << f.den() ;
}
 
bool operator==(const Fraction& lhs, const Fraction& rhs)
{
    return lhs.num() == rhs.num() && lhs.den() == rhs.den();
}
 
bool operator!=(const Fraction& lhs, const Fraction& rhs)
{
    return !(lhs == rhs);
}
 
Fraction operator*(Fraction lhs, const Fraction& rhs)
{
    return lhs *= rhs;
}
 
int main()
{
    Fraction f1(3, 8), f2(1, 2), f3(10, 2);
    std::cout << f1 << " * " << f2 << " = " << f1 * f2 << '\n'
              << f2 << " * " << f3 << " = " << f2 * f3 << '\n'
              <<  2 << " * " << f1 << " = " <<  2 * f1 << '\n';
}

输出:

3/8 * 1/2 = 3/16
1/2 * 5/1 = 5/2
2 * 3/8 = 3/4

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 1481 C++98 非成员前缀自增运算符的形参只能具有类或枚举类型 类型没有限制

参阅

常见运算符
赋值 自增/自减 算术 逻辑 比较 成员访问 其他

a = b
a += b
a -= b
a *= b
a /= b
a %= b
a &= b
a |= b
a ^= b
a <<= b
a >>= b

++a
--a
a++
a--

+a
-a
a + b
a - b
a * b
a / b
a % b
~a
a & b
a | b
a ^ b
a << b
a >> b

!a
a && b
a || b

a == b
a != b
a < b
a > b
a <= b
a >= b
a <=> b

a[b]
*a
&a
a->b
a.b
a->*b
a.*b

函数调用
a(...)
逗号
a, b
条件
a ? b : c
特殊运算符

static_cast 转换一个类型为另一相关类型
dynamic_cast 在继承层级中转换
const_cast 添加或移除 cv 限定符
reinterpret_cast 转换类型到无关类型
C 风格转型static_castconst_castreinterpret_cast 的混合转换一个类型到另一类型
new 创建有动态存储期的对象
delete 销毁先前由 new 表达式创建的对象,并释放其所拥有的内存区域
sizeof 查询类型的大小
sizeof... 查询形参包的大小(C++11 起)
typeid 查询类型的类型信息
noexcept 查询表达式是否能抛出异常(C++11 起)
alignof 查询类型的对齐要求(C++11 起)

引用

  1. StackOverflow C++ FAQ 上的运算符重载