21.10 — 重载圆括号运算符

到目前为止,你所见过的所有重载运算符都允许你定义运算符参数的类型,但不能定义参数的数量(参数数量是根据运算符的类型固定的)。例如,operator== 总是接受两个参数,而 operator! 总是接受一个参数。圆括号运算符 (operator()) 是一个特别有趣的运算符,因为它允许你更改其接受的参数类型和数量。

需要记住两件事:首先,圆括号运算符必须作为成员函数实现。其次,在非面向对象的 C++ 中,() 运算符用于调用函数。对于类,operator() 只是一个普通的运算符,它像任何其他重载运算符一样调用一个函数(名为 operator())。

一个例子

让我们看一个适合重载此运算符的例子

class Matrix
{
private:
    double data[4][4]{};
};

矩阵是线性代数的关键组成部分,常用于几何建模和 3D 计算机图形工作。在这种情况下,你只需要认识到 Matrix 类是一个 4 乘 4 的二维双精度浮点数数组。

在关于重载下标运算符的课程中,你学到我们可以重载 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; // for const objects
};

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

现在,让我们再次重载 () 运算符,这次是以不带任何参数的方式

#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()()
{
    // reset all elements of the matrix to 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(); // erase matrix
    std::cout << matrix(1, 2) << '\n';

    return 0;
}

结果是

0

由于 () 运算符如此灵活,它可能很容易被用于许多不同的目的。然而,强烈不鼓励这样做,因为 () 符号并没有真正说明运算符正在做什么。在上面的例子中,最好将擦除功能写成一个名为 clear() 或 erase() 的函数,因为 matrix.erase()matrix()(它可以做任何事情!)更容易理解。

注意:从 C++23 开始,你可以使用带有多个索引的 operator[]。这与上面 operator() 的工作方式类似。

函数对象(Functors)的乐趣

Operator() 也常被重载来实现函数对象(或函数对象),它们是像函数一样操作的类。函数对象优于普通函数的地方在于,函数对象可以将数据存储在成员变量中(因为它们是类)。

这是一个简单的函数对象

#include <iostream>

class Accumulator
{
private:
    int m_counter{ 0 };

public:
    int operator() (int i) { return (m_counter += i); }

    void reset() { m_counter = 0; } // optional 
};

int main()
{
    Accumulator acc{};
    std::cout << acc(1) << '\n'; // prints 1
    std::cout << acc(3) << '\n'; // prints 4

    Accumulator acc2{};
    std::cout << acc2(10) << '\n'; // prints 10
    std::cout << acc2(20) << '\n'; // prints 30
    
    return 0;
}

请注意,使用我们的 Accumulator 看起来就像进行正常的函数调用,但我们的 Accumulator 对象正在存储一个累积值。

函数对象的好处在于,我们可以根据需要实例化任意数量的独立函数对象,并同时使用它们。函数对象还可以有其他成员函数(例如 reset())来执行方便的操作。

总结

Operator() 有时会重载两个参数,用于索引多维数组,或检索一维数组的子集(其中两个参数定义要返回的子集)。其他任何情况最好写成成员函数,并带有更具描述性的名称。

Operator() 也常被重载以创建函数对象。尽管简单的函数对象(如上面的示例)很容易理解,但函数对象通常用于更高级的编程主题,并值得单独一课。

小测验时间

问题 #1

编写一个名为 MyString 的类,它包含一个 std::string。重载 operator<< 以输出字符串。重载 operator() 以返回从第一个参数索引开始的子字符串(作为 MyString)。子字符串的长度应由第二个参数定义。

以下代码应该运行

int main()
{
    MyString s { "Hello, world!" };
    std::cout << s(7, 5) << '\n'; // start at index 7 and return 5 characters

    return 0;
}

这应该打印

world

提示:你可以使用 std::string::substr 来获取 std::string 的子字符串。

显示答案

问题 #2

此测验题为额外加分题。

> 步骤 #1

如果我们不需要修改返回的子字符串,为什么上述方法效率低下?

显示答案

> 步骤 #2

我们还能做什么?

显示答案

> 步骤 #3

更新之前测验解决方案中的 operator(),使其返回子字符串为 std::string_view

提示:std::string::substr() 返回一个 std::stringstd::string_view::substr() 返回一个 std::string_view。务必小心不要返回一个悬空(dangling)的 std::string_view

显示提示

显示提示

显示答案

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
249 评论
最新
最早 最多投票
内联反馈
查看所有评论