重载圆括号运算符

在此之前介绍的所有重载运算符,仅允许我们自定义其参数的类型,却不允许改变参数的数量(该数量由运算符本身固定)。例如,operator== 始终接受两个参数,而 operator! 始终接受一个。圆括号运算符(operator())则与众不同:它既允许我们改变参数的类型,也允许改变参数的数量。

需要牢记两点:

  1. 圆括号运算符必须作为成员函数实现。
  2. 在非面向对象 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!

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"