Namespaces
Variants

operator overloading

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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

目录

语法

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

operator op (1)
operator new
operator new []
(2)
operator delete
operator delete []
(3)
operator co_await (4) (自 C++20 起)
op - 可为以下任意运算符: + - * / % ^ & | ~ ! = < > + = - = * = / = % = ^ = & = | = << >> >>= <<= == ! = <= >= <=> (C++20 起) && || ++ -- , - > * - > ( ) [ ]
1) 一个重载的标点运算符。
2) 一个 分配函数
3) 一个 解分配函数
4) 用于 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 起)

重载运算符(但不包括内置运算符)可以通过函数表示法调用:

std::string str = "Hello, ";
str.operator+=("world");                      // 等同于 str += "world";
operator<<(operator<<(std::cout, str), '\n'); // 等同于 std::cout << str << '\n';
                                              // (C++17起) 除序列点外完全一致

静态重载运算符

作为成员函数的重载运算符可以声明为 静态 。但这仅适用于 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]

赋值运算符

赋值运算符 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;
}

规范的移动赋值操作应当: 使被移动对象处于有效状态 (即保持类不变量的状态),并且在自赋值时 不执行操作 或至少使对象保持有效状态,同时返回非常量左值的引用,且标记为 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;
}
(since C++11)

在那些拷贝赋值无法受益于资源复用的情况下(即不管理堆分配数组且没有(可能传递性的)成员管理此类资源,例如成员 std::vector std::string ),存在一种流行且便捷的简写方式:拷贝交换赋值运算符。该运算符通过值接收参数(从而根据实参的值类别同时实现拷贝赋值和移动赋值),与参数进行交换,并让析构函数完成清理工作。

// 复制赋值(复制并交换惯用法)
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)
{
    // 将对象写入流
    return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
    // 从流中读取对象
    if (/* 无法构造 T */)
        is.setstate(std::ios::failbit);
    return is;
}

这些运算符有时被实现为 friend functions

函数调用运算符

当用户自定义类重载了函数调用运算符 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);
}

许多标准库 算法 接受 函数对象 来自定义行为。 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

自增与自减

当后缀递增或递减运算符出现在表达式中时,会调用对应的用户定义函数( operator ++ operator -- )并传入整型参数 0 。通常其声明形式为 T operator ++ ( int ) 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 + 是复数类型的成员函数,则仅 complex + integer 能通过编译,而 integer + complex 则不能)。由于每个二元算术运算符都存在对应的复合赋值运算符,二元运算符的规范实现通常基于其复合赋值运算来实现:

class X
{
public:
    X& operator+=(const X& rhs) // 复合赋值(不必是成员函数,
    {                           // 但通常作为成员函数以修改私有成员)
        /* 此处实现 rhs 与 *this 的相加操作 */
        return *this; // 通过引用返回结果
    }
    // 在类体内定义的友元函数是内联的,且对非ADL查找隐藏
    friend X operator+(X lhs,        // 按值传递 lhs 有助于优化链式运算 a+b+c
                       const X& rhs) // 否则两个参数都应为常引用
    {
        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::memcmp std::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; }
**说明:** - 所有C++代码(位于`
`标签内)已按规则保留原文未翻译
- 保留了所有HTML标签和属性
- 遵循了不翻译专业术语的要求
- 仅添加了中文说明文本

数组下标运算符

提供类似数组读写访问的用户自定义类通常需要为 operator [ ] 定义两个重载版本:常量版本与非常量版本:

struct T
{
          value_t& operator[](std::size_t idx)       { return mVector[idx]; }
    const value_t& operator[](std::size_t idx) const { return mVector[idx]; }
};
(注:根据要求,代码块内的C++代码内容未进行翻译,仅提供结构说明) 此代码定义了一个结构体T,包含两个重载的下标运算符: - 非常量版本返回value_t类型的可修改引用 - 常量版本返回value_t类型的常量引用 两个运算符均通过mVector成员访问指定索引的元素

或者,它们也可以通过使用 显式对象形参 的单个成员函数模板来表示:

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 [ ] 只能接受一个下标。为实现多维数组访问语义,例如实现三维数组访问 a [ i ] [ j ] [ k ] = x ; operator [ ] 必须返回对二维平面的引用,该平面又需要有自己的 operator [ ] 返回对一维行的引用,而该行又需要具有 operator [ ] 返回对元素的引用。为避免这种复杂性,某些库选择重载 operator ( ) ,使得三维访问表达式具有类似 Fortran 的语法 a ( i, j, k ) = x ;

(C++23 前)

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

#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 起)

位运算算术运算符

实现 BitmaskType 要求的用户自定义类与枚举类型,必须重载位运算运算符 operator & operator | operator ^ operator~ operator & = operator | = operator ^ = ,并可选择性地重载移位运算符 operator << operator >> operator >>= operator <<= 。其规范实现通常遵循前述二元算术运算符的模式。

布尔取反运算符

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

(C++11 前)

由于内置运算符 ! 会执行 bool 的上下文转换operator bool 而无需重载 operator !

(C++11 起)

罕见重载运算符

以下运算符很少被重载:

  • 取址运算符 operator & 。若将一元 & 应用于不完整类型的左值,且其完整类型声明了重载的 operator & ,则未指定该运算符是具有内置含义还是调用运算符函数。由于此运算符可能被重载,泛型库使用 std::addressof 来获取用户定义类型对象的地址。最著名的规范重载 operator & 实例是微软的 CComPtrBase 类。此运算符在 EDSL 中的使用示例可见于 boost.spirit
  • 布尔逻辑运算符 operator && operator || 。与内置版本不同,重载版本无法实现短路求值。 同时与内置版本不同的是,它们不会在右操作数之前对左操作数进行定序。 (C++17 前) 在标准库中,这些运算符仅针对 std::valarray 进行了重载。
  • 逗号运算符 operator, 与内置版本不同,重载版本不会在右操作数之前对左操作数进行定序。 (C++17 前) 由于此运算符可能被重载,泛型库使用如 a, void ( ) , b 而非 a, b 的表达式来对用户定义类型的表达式执行序列化。boost 库在 boost.assign boost.spirit 及其他库中使用了 operator, 。数据库访问库 SOCI 也重载了 operator,
  • 通过成员指针访问成员的运算符 operator - > * 。重载此运算符没有特定缺点,但在实践中很少使用。有建议认为它可以作为 智能指针接口 的组成部分,实际上 boost.phoenix 中的执行器就以此方式使用它。在如 cpp.react 等 EDSL 中更为常见。

注释

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

关键词

operator

示例

#include <iostream>
class Fraction
{
    // 或 C++17 的 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

缺陷报告

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

缺陷报告 适用标准 发布行为 正确行为
CWG 1481 C++98 非成员前缀递增运算符的参数只能是类类型、枚举类型或这些类型的引用类型 无类型限制
CWG 2931 C++23 显式对象成员运算符函数只能具有无类类型、枚举类型或这些类型的引用类型的参数 禁止

参见

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

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_cast const_cast reinterpret_cast 将一种类型转换为另一种类型
new 创建具有动态存储期的对象
delete 销毁先前由 new 表达式创建的对象并释放获取的内存区域
sizeof 查询类型的大小
sizeof... 查询 参数包 的大小 (C++11 起)
typeid 查询类型的类型信息
noexcept 检查表达式是否能抛出异常 (C++11 起)
alignof 查询类型的对齐要求 (C++11 起)

外部链接

  1. 运算符重载 在 StackOverflow C++ 常见问题解答中