C++运算符重载的基本规则和惯用法

C++运算符重载的基本规则和惯用法

技术背景

在C++中,运算符重载允许程序员为自定义类型重新定义运算符的行为,使得代码更加直观和易于理解。然而,运算符重载需要遵循一定的规则和惯用法,以确保代码的正确性和可维护性。

实现步骤

1. 遵循基本规则

  • 避免不明确的重载:当运算符的含义不明确且有争议时,不应进行重载,而应提供一个命名良好的函数。
  • 保持运算符的常见语义:重载运算符时,应遵循其在常规使用中的语义,避免让用户产生意外的结果。
  • 提供相关操作:如果类型支持某个运算符,用户通常期望能使用与其相关的其他运算符,因此应提供完整的相关操作。

2. 选择成员或非成员函数

  • 必须为成员函数的运算符[]()=-> 等运算符必须实现为成员函数。
  • 通常为非成员函数的运算符:输入和输出运算符 <<>> 通常需要实现为非成员函数,因为其左操作数是标准库中的流类,无法添加成员函数。
  • 其他运算符的选择规则
    • 一元运算符通常实现为成员函数。
    • 对两个操作数同等对待的二元运算符通常实现为非成员函数。
    • 对两个操作数处理方式不同(通常会修改左操作数)的二元运算符,若需要访问操作数的私有部分,可实现为左操作数类型的成员函数。

3. 重载常见运算符

  • 赋值运算符:使用复制 - 交换惯用法实现赋值运算符。
1
2
3
4
5
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
  • 流插入和提取运算符:实现为非成员函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
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( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
  • 函数调用运算符:必须定义为成员函数。
1
2
3
4
5
6
struct X {
// Overloaded call operator
int operator()(const std::string& y) {
return /* ... */;
}
};
  • 比较运算符:在C++20中,可通过默认 operator<=> 来重载所有比较运算符。
1
2
3
4
5
6
#include <compare>

struct X {
// defines ==, !=, <, >, <=, >=, <=>
friend auto operator<=>(const X&, const X&) = default;
};
  • 逻辑运算符:一元前缀否定 ! 通常实现为成员函数,二元逻辑运算符 ||&& 通常实现为非成员函数,但很少有合理的使用场景。
  • 算术运算符
    • 一元算术运算符:重载递增和递减运算符时,应同时实现前缀和后缀版本。
1
2
3
4
5
6
7
8
9
10
11
12
13
struct X {
X& operator++()
{
// Do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
- 二元算术运算符:实现 `+` 时,通常基于 `+=` 来实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};

inline X operator+(const X& lhs, const X& rhs)
{
X result = lhs;
result += rhs;
return result;
}
  • 下标运算符:必须实现为类成员,通常提供常量和非常量版本。
1
2
3
4
5
struct X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
  • 指针类类型的运算符:重载一元前缀解引用运算符 * 和二元中缀指针成员访问运算符 ->,通常需要常量和非常量版本。
1
2
3
4
5
6
struct my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};

4. 转换运算符

  • 隐式转换运算符:允许编译器隐式地将用户定义类型的值转换为其他类型,但可能会导致意外的结果。
1
2
3
4
5
6
class my_string {
public:
operator const char*() const {return data_;} // This is the conversion operator
private:
const char* data_;
};
  • 显式转换运算符:避免了隐式转换带来的问题,需要使用 static_cast 等显式转换。
1
2
3
4
5
6
class my_string {
public:
explicit operator const char*() const {return data_;}
private:
const char* data_;
};

5. 重载 newdelete 运算符

  • 基本规则:重载 newdelete 运算符通常是为了解决性能问题和内存约束,应同时重载匹配的 operator delete
1
2
3
4
void* operator new(std::size_t) throw(std::bad_alloc); 
void operator delete(void*) throw();
void* operator new[](std::size_t) throw(std::bad_alloc);
void operator delete[](void*) throw();
  • 定位 new:允许在指定地址创建对象。
1
2
3
4
5
6
7
8
class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{
X* p = new(buffer) X(/*...*/);
// ...
p->~X(); // call destructor
}
  • 类特定的 newdelete:可以为特定类重载 newdelete 以优化内存管理。
1
2
3
4
5
6
7
8
9
class my_class { 
public:
// ...
void* operator new(std::size_t);
void operator delete(void*);
void* operator new[](std::size_t);
void operator delete[](void*);
// ...
};

最佳实践

  • 遵循规则:严格遵循运算符重载的基本规则,确保代码的正确性和可维护性。
  • 使用默认比较:在C++20中,优先使用默认比较运算符,减少样板代码。
  • 考虑性能:在实现运算符重载时,考虑性能因素,如优先使用 += 而不是 +

常见问题

  • 运算符选择错误:选择成员函数或非成员函数实现运算符时出错,导致代码无法编译或行为不符合预期。
  • 语义不一致:重载运算符时,违反了运算符的常见语义,使代码难以理解和维护。
  • 遗漏相关操作:没有提供完整的相关操作,导致用户代码无法正常使用。

C++运算符重载的基本规则和惯用法
https://119291.xyz/posts/2025-05-14.c-plus-plus-operator-overloading-rules-and-idioms/
作者
ww
发布于
2025年5月14日
许可协议