命名空间
变体
操作

运算符重载

来自 cppreference.com
< cpp‎ | language
 
 
C++ 语言
 
 

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

内容

[编辑] 语法

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

operator op (1)
operator type (2)
operator new
operator new []
(3)
operator delete
operator delete []
(4)
operator "" suffix-identifier (5) (自 C++11 起)
operator co_await (6) (自 C++20 起)
op - 以下任何一个运算符:+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=>(自 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-> ( ) 不能是非成员 给定 std::unique_ptr<S> p;p->bar() 调用 p.operator->()
a@ (a).operator@ (0) operator@ (a, 0) 给定 std::vector<int>::iterator i;i++ 调用 i.operator++(0)

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

此外,对于比较运算符 ==!=<><=>=<=>,重载解析还会考虑从 operator==operator<=> 生成的重写候选

(自 C++20 起)

注意:对于重载 co_await(自 C++20 起)用户定义的转换函数用户定义的字面量分配释放,请参阅各自的文章。

重载运算符(但不是内置运算符)可以使用函数符号调用。

std::string str = "Hello, ";
str.operator+=("world");                      // same as str += "world";
operator<<(operator<<(std::cout, str), '\n'); // same as std::cout << str << '\n';
                                              // (since C++17) except for sequencing

静态重载运算符

作为成员函数的重载运算符可以声明为 静态。但是,这仅允许用于 operator()operator[]

此类运算符可以使用函数符号调用。但是,当这些运算符出现在表达式中时,它们仍然需要一个类类型的对象。

struct SwapThem {
    template <typename T>
    static void operator()(T& lhs, T& rhs) 
    {
        std::ranges::swap(lhs, rhs);
    }
    template <typename T>
    static void operator[](T& lhs, T& rhs)
    {
        std::ranges::swap(lhs, rhs);
    } 
};
inline constexpr SwapThem swap_them {};
 
void foo()
{
    int a = 1, b = 2;
 
    swap_them(a, b); // OK
    swap_them[a, b]; // OK
 
    SwapThem{}(a, b); // OK
    SwapThem{}[a, b]; // OK
 
    SwapThem::operator()(a, b); // OK
    SwapThem::operator[](a, b); // OK
 
    SwapThem(a, b); // error, invalid construction
    SwapThem[a, b]; // error
}
(自 C++23 起)

[编辑] 限制

  • 运算符 ::(作用域解析)、.(成员访问)、.*(通过指向成员的指针进行成员访问)和 ?:(三元条件)不能被重载。
  • 不能创建诸如 **<>&| 之类的新的运算符。
  • 无法更改运算符的优先级、分组或操作数数量。
  • 运算符 -> 的重载必须返回一个原始指针,或者返回一个对象(通过引用或按值),而该对象反过来重载了运算符 ->
  • 运算符 &&|| 的重载会失去短路求值。
  • &&||,(逗号)在重载时会失去其特殊的 排序属性,即使在没有使用函数调用符号的情况下使用它们,它们的行为也像普通的函数调用。
(直到 C++17)

[编辑] 规范实现

除了上述限制之外,该语言对重载运算符的执行方式或返回类型没有其他约束(它不参与重载解析),但通常,重载运算符应尽可能与内置运算符的行为相似:operator+ 应该对参数进行加法运算,而不是乘法运算,operator= 应该进行赋值运算,等等。相关的运算符应该有类似的行为(operator+operator+= 执行相同的加法运算)。返回类型受运算符预期使用的表达式限制:例如,赋值运算符通过引用返回,以便可以编写 a = b = c = d,因为内置运算符允许这样做。

常用的重载运算符有以下典型的规范形式:[1]

[edit] 赋值运算符

赋值运算符(operator=)具有特殊属性:有关详细信息,请参阅 复制赋值移动赋值

规范的复制赋值运算符应 对自赋值安全,并通过引用返回左值。

// copy assignment
T& operator=(const T& other)
{
    // Guard self assignment
    if (this == &other)
        return *this;
 
    // assume *this manages a reusable resource, such as a heap-allocated buffer mArray
    if (size != other.size)           // resource in *this cannot be reused
    {
        temp = new int[other.size];   // allocate resource, if throws, do nothing
        delete[] mArray;              // release resource in *this
        mArray = temp;
        size = other.size;
    } 
 
    std::copy(other.mArray, other.mArray + other.size, mArray);
    return *this;
}

规范的移动赋值运算符应 将移动后的对象留在有效状态(即,状态具有完整的类不变式),并且要么 不执行任何操作,或者至少在自赋值时将对象留在有效状态,并通过引用返回左值,并声明为 noexcept。

// move assignment
T& operator=(T&& other) noexcept
{
    // Guard self assignment
    if (this == &other)
        return *this; // delete[]/size=0 would also be ok
 
    delete[] mArray;                               // release resource in *this
    mArray = std::exchange(other.mArray, nullptr); // leave other in valid state
    size = std::exchange(other.size, 0);
    return *this;
}
(自 C++11 起)

在复制赋值无法从资源重用中受益的情况下(它不管理堆分配的数组,也不具有(可能可传递的)管理堆分配数组的成员,例如成员 std::vectorstd::string),有一种流行的简便方法:复制和交换赋值运算符,它按值获取参数(因此根据参数的值类别,既充当复制赋值,又充当移动赋值),与参数进行交换,并让析构函数清理它。

// copy assignment (copy-and-swap idiom)
T& T::operator=(T other) noexcept // call copy or move constructor to construct other
{
    std::swap(size, other.size); // exchange resources between *this and other
    std::swap(mArray, other.mArray);
    return *this;
} // destructor of other is called to release the resources formerly managed by *this

此形式自动提供 强异常保证,但禁止资源重用。

[edit] 流提取和插入

接受 std::istream&std::ostream& 作为左操作数的 operator>>operator<< 的重载称为插入和提取运算符。由于它们将用户定义类型作为右操作数(在 a @ b 中为 b),因此必须将它们实现为非成员函数。

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 (/* T could not be constructed */)
        is.setstate(std::ios::failbit);
    return is;
}

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

[edit] 函数调用运算符

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

这种类型的对象可以在函数调用表达式中使用。

// An object of this type represents a linear function of one variable a * x + b.
struct Linear
{
    double a, b;
 
    double operator()(double x) const
    {
        return a * x + b;
    }
};
 
int main()
{
    Linear f{2, 1};  // Represents function 2x + 1.
    Linear g{-1, 0}; // Represents function -x.
    // f and g are objects that can be used like a function.
 
    double f_0 = f(0);
    double f_1 = f(1);
 
    double g_0 = g(0);
}

std::sortstd::accumulate 的许多标准算法都接受 FunctionObjects 来定制行为。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 << "The sum is " << s.sum << '\n';
}

输出

The sum is 15

[edit] 递增和递减

当后缀递增或递减运算符出现在表达式中时,相应的用户定义函数(operator++operator--)将被调用,并带有一个整数参数 0。通常,它被实现为 T operator++(int)T operator--(int),其中参数被忽略。后缀递增和递减运算符通常根据前缀版本实现。

struct X
{
    // prefix increment
    X& operator++()
    {
        // actual increment takes place here
        return *this; // return new value by reference
    }
 
    // postfix increment
    X operator++(int)
    {
        X old = *this; // copy old value
        operator++();  // prefix increment
        return old;    // return old value
    }
 
    // prefix decrement
    X& operator--()
    {
        // actual decrement takes place here
        return *this; // return new value by reference
    }
 
    // postfix decrement
    X operator--(int)
    {
        X old = *this; // copy old value
        operator--();  // prefix decrement
        return old;    // return old value
    }
};

虽然前缀递增和递减运算符的规范实现通过引用返回,但与任何运算符重载一样,返回类型是用户定义的;例如,这些运算符对 std::atomic 的重载按值返回。

[edit] 二元算术运算符

二元运算符通常被实现为非成员函数,以保持对称性(例如,在添加复数和整数时,如果 operator+ 是复数类型的成员函数,那么只有 complex + integer 会编译,而不是 integer + complex)。由于对于每个二元算术运算符,都存在一个对应的复合赋值运算符,因此二元运算符的规范形式是根据它们的复合赋值实现的。

class X
{
public:
    X& operator+=(const X& rhs) // compound assignment (does not need to be a member,
    {                           // but often is, to modify the private members)
        /* addition of rhs to *this takes place here */
        return *this; // return the result by reference
    }
 
    // friends defined inside class body are inline and are hidden from non-ADL lookup
    friend X operator+(X lhs,        // passing lhs by value helps optimize chained a+b+c
                       const X& rhs) // otherwise, both parameters may be const references
    {
        lhs += rhs; // reuse compound assignment
        return lhs; // return the result by value (uses move constructor)
    }
};

[edit] 比较运算符

标准算法(如 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); // keep the same order
    }
};

通常,一旦提供 operator<,其他关系运算符将根据 operator< 实现。

inline bool operator< (const X& lhs, const X& rhs) { /* do actual comparison */ }
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) { /* do actual comparison */ }
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&) const = default;
};
// records can now be compared with ==, !=, <, <=, >, and >=

有关详细信息,请参阅 默认比较

(自 C++20 起)

[edit] 数组下标运算符

提供数组式访问的用户定义类(允许读写)通常为 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]; }
};

或者,可以使用 显式对象参数 将它们表示为单个成员函数模板。

struct T
{
    decltype(auto) operator[](this auto& self, std::size_t idx) 
    { 
        return self.mVector[idx]; 
    }
};
(自 C++23 起)

如果值类型已知为标量类型,则 const 变体应按值返回。

如果不需要或不可能直接访问容器的元素,或者需要区分左值 c[i] = v; 和右值 v = c[i]; 的用法,则 operator[] 可以返回代理。例如,请参阅 std::bitset::operator[]

operator[] 只能接受一个下标。为了提供多维数组访问语义,例如,要实现 3D 数组访问 a[i][j][k] = x;operator[] 必须返回对 2D 平面的引用,该平面必须有自己的 operator[],该运算符返回对 1D 行的引用,该行必须有 operator[],该运算符返回对元素的引用。为了避免这种复杂性,一些库选择重载 operator(),这样 3D 访问表达式将具有类似 Fortran 的语法 a(i, j, k) = x;

(截至 C++23)

operator[] 可以接受任意数量的下标。例如,一个声明为 T& operator[](std::size_t x, std::size_t y, std::size_t z); 的 3D 数组类的 operator[] 可以直接访问元素。

#include <array>
#include <cassert>
#include <iostream>
 
template<typename T, std::size_t Z, std::size_t Y, std::size_t X>
struct Array3d
{
    std::array<T, X * Y * Z> m{};
 
    constexpr T& operator[](std::size_t z, std::size_t y, std::size_t x) // C++23
    {
        assert(x < X and y < Y and z < Z);
        return m[z * Y * X + y * X + x];
    }
};
 
int main()
{
    Array3d<int, 4, 3, 2> v;
    v[3, 2, 1] = 42;
    std::cout << "v[3, 2, 1] = " << v[3, 2, 1] << '\n';
}

输出

v[3, 2, 1] = 42
(自 C++23 起)

[edit] 位运算符

实现 BitmaskType 要求的用户定义类和枚举需要重载位运算符 operator&operator|operator^operator~operator&=operator|=operator^=,并且可以选择重载移位运算符 operator<< operator>>operator>>=operator<<=。规范的实现通常遵循上面描述的二元算术运算符模式。

[edit] 布尔取反运算符

运算符 operator! 通常由旨在用于布尔上下文的用户定义类重载。此类类还提供了一个用户定义的转换为布尔类型的转换函数(参见 std::basic_ios 获取标准库示例),并且 operator! 的预期行为是返回与 operator bool 相反的值。

(直到 C++11)

由于内置运算符 ! 执行 隐式转换为 bool,旨在用于布尔上下文的用户定义类可以仅提供 operator bool 而不必重载 operator!

(自 C++11 起)

[edit] 很少重载的运算符

以下运算符很少重载

  • 取地址运算符 operator&。如果对不完整类型的左值应用一元 &,并且完整类型声明了一个重载的 operator&,则该运算符是否具有内置含义或调用运算符函数是未指定的。因为此运算符可能被重载,所以通用库使用 std::addressof 来获取用户定义类型对象的地址。最著名的规范重载运算符& 示例是 Microsoft 类 CComPtrBase。在 EDSL 中,此运算符的使用示例可以在 boost.spirit 中找到。
  • 布尔逻辑运算符 operator&&operator||。与内置版本不同,重载不能实现短路求值。 此外,与内置版本不同,它们不会在右操作数之前对左操作数进行排序。(直到 C++17) 在标准库中,这些运算符只针对 std::valarray 重载。
  • 逗号运算符 operator,与内置版本不同,重载不会在右操作数之前对左操作数进行排序。(直到 C++17) 因为此运算符可能被重载,所以通用库使用诸如 a,void(),b 的表达式而不是 a,b 来对用户定义类型表达式的执行进行排序。boost 库在 boost.assignboost.spirit 和其他库中使用 operator,。数据库访问库 SOCI 也重载了 operator,
  • 通过指向成员的指针的成员访问 operator->*。重载此运算符没有具体缺点,但实际上很少使用。有人建议它可以作为 智能指针接口 的一部分,实际上,它在 boost.phoenix 中以该身份使用。在诸如 cpp.react 的 EDSL 中,它更为常见。

[edit] 备注

功能测试 Std 功能
__cpp_static_call_operator 202207L (C++23) static operator()
__cpp_multidimensional_subscript 202211L (C++23) static operator[]

[edit] 关键字

operator

[edit] 示例

#include <iostream>
 
class Fraction
{
    // or C++17's std::gcd
    constexpr int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
 
    int n, d;
public:
    constexpr Fraction(int n, int d = 1) : n(n / gcd(n, d)), d(d / gcd(n, d)) {}
 
    constexpr int num() const { return n; }
    constexpr int den() const { return d; }
 
    constexpr 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();
}
 
constexpr bool operator==(const Fraction& lhs, const Fraction& rhs)
{
    return lhs.num() == rhs.num() && lhs.den() == rhs.den();
}
 
constexpr bool operator!=(const Fraction& lhs, const Fraction& rhs)
{
    return !(lhs == rhs);
}
 
constexpr Fraction operator*(Fraction lhs, const Fraction& rhs)
{
    return lhs *= rhs;
}
 
int main()
{
    constexpr 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';
    static_assert(f3 == f2 * 10);
}

输出

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

[edit] 缺陷报告

以下更改行为的缺陷报告被追溯地应用于先前发布的 C++ 标准。

DR 应用于 已发布的行为 正确的行为
CWG 1481 C++98 非成员前缀递增运算符可以
只有类或枚举类型的参数
没有类型要求

[edit] 参见

常用运算符
赋值 递增
递减
算术 逻辑 比较 成员
访问
其他

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[...]
*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++ 常见问题解答