命名空间
变体
操作

运算符重载

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

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

目录

[编辑] 语法

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

operator op (1)
operator new
operator new []
(2)
operator delete
operator delete []
(3)
operator co_await (4) (C++20 起)
op - 以下任一运算符:+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=>(C++20起) && || ++ -- , ->* -> () []
1) 重载的标点符号运算符。
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");                      // 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]

[编辑] 赋值运算符

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

规范的移动赋值预期将移动源对象置于有效状态(即,类不变量保持不变的状态),并且在自赋值时不做任何事情或至少将对象置于有效状态,并通过对非 const 的引用返回左值,并且为 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

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

[编辑] 流提取和插入

接受 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;
}

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

[编辑] 函数调用运算符

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

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

// 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);
}

许多标准库算法接受函数对象以自定义行为。 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
{
    // 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 的这些运算符的重载通过值返回。

[编辑] 二元算术运算符

二元运算符通常实现为非成员函数以保持对称性(例如,当添加一个复数和一个整数时,如果 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)
    }
};

[编辑] 比较运算符

标准库算法,例如 std::sort,以及容器,例如 std::set,默认要求为用户提供的类型定义 operator<,并要求它实现严格弱序(从而满足比较要求)。为结构实现严格弱序的惯用方法是使用 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[] 定义两个重载: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[] 可以接受任意数量的下标。例如,一个 3D 数组类的 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 起)

[编辑] 位算术运算符

实现位掩码类型要求的用户定义类和枚举需要重载位算术运算符 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& 最著名的例子是 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 中的 actor 就以这种方式使用它。在 EDSLs(例如 cpp.react)中更常见。

[编辑] 注意

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

[编辑] 关键词

operator

[编辑] 示例

#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

[编辑] 缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 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_castconst_castreinterpret_cast 的混合将一种类型转换为另一种类型
new 创建具有动态存储期的对象
delete 销毁先前由 new 表达式创建的对象并释放获得的内存区域
sizeof 查询类型的大小
sizeof... 查询 的大小 (C++11 起)
typeid 查询类型的类型信息
noexcept 检查表达式是否可以抛出异常 (C++11 起)
alignof 查询类型的对齐要求 (C++11 起)

[编辑] 外部链接

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