在此之前介绍的所有重载运算符,仅允许我们自定义其参数的类型,却不允许改变参数的数量(该数量由运算符本身固定)。例如,operator== 始终接受两个参数,而 operator! 始终接受一个。圆括号运算符(operator())则与众不同:它既允许我们改变参数的类型,也允许改变参数的数量。
需要牢记两点:
- 圆括号运算符必须作为成员函数实现。
- 在非面向对象 C++ 中,() 运算符用于调用函数;在类中,operator() 只是一个普通运算符,与其他重载运算符一样,调用名为 operator() 的函数。
示例
下面通过一个示例展示该运算符的重载:
class Matrix
{
private:
double data[4][4]{};
};
矩阵是线性代数的核心,常用于几何建模与三维图形计算。此处只需知道 Matrix 类内部是一个 4×4 的二维 double 数组。
在operator[]
直接访问私有的一维数组。然而,对于二维数组,C++23 之前 operator[]
仅限于单个参数,无法直接索引二维结构。
而 operator() 可接受任意数量的参数,因此可声明一个接收两个整数索引的版本,用于访问二维数组。示例如下:
#include <cassert> // for assert()
class Matrix
{
private:
double m_data[4][4]{};
public:
double& operator()(int row, int col);
double operator()(int row, int col) const; // 针对 const 对象
};
double& Matrix::operator()(int row, int col)
{
assert(row >= 0 && row < 4);
assert(col >= 0 && col < 4);
return m_data[row][col];
}
double Matrix::operator()(int row, int col) const
{
assert(row >= 0 && row < 4);
assert(col >= 0 && col < 4);
return m_data[row][col];
}
于是可以这样声明并访问 Matrix 元素:
#include <iostream>
int main()
{
Matrix matrix;
matrix(1, 2) = 4.5;
std::cout << matrix(1, 2) << '\n';
return 0;
}
输出:
4.5
再次重载 operator(),这一次不接受任何参数:
#include <cassert> // for assert()
class Matrix
{
private:
double m_data[4][4]{};
public:
double& operator()(int row, int col);
double operator()(int row, int col) const;
void operator()();
};
double& Matrix::operator()(int row, int col)
{
assert(row >= 0 && row < 4);
assert(col >= 0 && col < 4);
return m_data[row][col];
}
double Matrix::operator()(int row, int col) const
{
assert(row >= 0 && row < 4);
assert(col >= 0 && col < 4);
return m_data[row][col];
}
void Matrix::operator()()
{
// 将所有元素重置为 0.0
for (int row{ 0 }; row < 4; ++row)
{
for (int col{ 0 }; col < 4; ++col)
{
m_data[row][col] = 0.0;
}
}
}
新示例:
#include <iostream>
int main()
{
Matrix matrix{};
matrix(1, 2) = 4.5;
matrix(); // 擦除矩阵
std::cout << matrix(1, 2) << '\n';
return 0;
}
输出:
0
由于 operator() 极为灵活,有人倾向于将其用于多种用途。然而,这种做法极不推荐:符号 () 本身无法表明操作含义。上例中,擦除功能更宜写成 clear() 或 erase() 成员函数;matrix.erase()
明显优于 matrix()
(后者含义不明)。
注:自 C++23 起,operator[] 支持多个索引,用法与上述 operator() 相同。
与函数对象(functor)的乐趣
operator() 亦常用于实现函数对象(functor)。函数对象即行为类似函数的类,其优势在于可像类一样存储成员变量。
简单 functor 示例:
#include <iostream>
class Accumulator
{
private:
int m_counter{ 0 };
public:
int operator() (int i) { return (m_counter += i); }
void reset() { m_counter = 0; } // 可选
};
int main()
{
Accumulator acc{};
std::cout << acc(1) << '\n'; // 输出 1
std::cout << acc(3) << '\n'; // 输出 4
Accumulator acc2{};
std::cout << acc2(10) << '\n'; // 输出 10
std::cout << acc2(20) << '\n'; // 输出 30
return 0;
}
注意:使用 Accumulator 形如普通函数调用,但对象内部保存累积值。
函数对象优点:可实例化任意多个独立对象并同时使用;亦可拥有其他成员函数(如 reset())以提供便利操作。
结论
- 若需索引多维数组,或按两参数取一维数组子集,可用双参 operator();其余情境更宜写成具描述性名称的成员函数。
- operator() 亦常用于创建函数对象。简单 functor 易于理解,但 functor 多用于高级主题,值得另设专课。
测验
问题 1
编写名为 MyString 的类,内部持有 std::string。重载 operator« 以输出字符串;重载 operator() 返回从首参数指定索引开始的子串(类型为 MyString),子串长度由第二参数决定。
应能运行以下代码:
int main()
{
MyString s{ "Hello, world!" };
std::cout << s(7, 5) << '\n'; // 从索引 7 开始取 5 个字符
return 0;
}
应输出:
world
提示:可用 std::string::substr 获取子串。
问题 2(加分题)
步骤 1
若无需修改返回的子串,上述实现为何低效?
步骤 2
可如何改进?
步骤 3
请将上一题 operator() 改为返回 std::string_view。
提示:std::string::substr() 返回 std::string;std::string_view::substr() 返回 std::string_view。注意勿返回悬空的 std::string_view!