13.10 – Перегрузка оператора круглые скобки ()

Добавлено 19 июля 2021 в 14:06

Все перегруженные операторы, которые мы рассмотрели на данный момент, позволяют вам определять тип параметров оператора, но не количество параметров (которое фиксируется в зависимости от типа оператора). Например, operator== всегда принимает два параметра, а operator! всегда принимает один. Оператор круглые скобки (operator()) является особенно интересным оператором, поскольку он позволяет вам изменять как тип, так и количество принимаемых параметров.

Следует помнить о двух вещах: во-первых, оператор круглые скобки должен быть реализован как функция-член. Во-вторых, в не объектно-ориентированном C++ operator() используется для вызова функций. В случае классов operator() – это просто обычный оператор, вызывающий функцию (с именем operator()), как и любой другой перегруженный оператор.

Пример

Давайте посмотрим на пример, который дает возможность перегрузить этот оператор:

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

Матрицы являются ключевым компонентом линейной алгебры и часто используются для геометрического моделирования и компьютерной 3D графики. В этом случае всё, что вам нужно знать, это то, что класс Matrix представляет собой двумерный массив чисел типа double размером 4 на 4.

В уроке о перегрузке оператора индекса вы узнали, что мы можем перегрузить operator[], чтобы обеспечить прямой доступ к закрытому одномерному массиву. Однако в этом случае нам нужен доступ к закрытому двумерному массиву. Поскольку operator[] ограничен одним параметром, его недостаточно, чтобы указывать на элемент двумерного массива.

Однако, поскольку оператор () может принимать столько параметров, сколько мы хотим, мы можем объявить версию operator(), которая принимает два целочисленных параметра индекса, и использовать ее для доступа к нашему двумерному массиву. Например:

#include <cassert> // для assert()
 
class Matrix
{
private:
    double m_data[4][4]{};
 
public:
    double& operator()(int row, int col);
    double operator()(int row, int col) const; // для константных объектов
};
 
double& Matrix::operator()(int row, int col)
{
    assert(col >= 0 && col < 4);
    assert(row >= 0 && row < 4);
 
    return m_data[row][col];
}
 
double Matrix::operator()(int row, int col) const
{
    assert(col >= 0 && col < 4);
    assert(row >= 0 && row < 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> // для 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(col >= 0 && col < 4);
    assert(row >= 0 && row < 4);
 
    return m_data[row][col];
}
 
double Matrix::operator()(int row, int col) const
{
    assert(col >= 0 && col < 4);
    assert(row >= 0 && row < 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() (что может делать что угодно!).

Развлекаемся с функторами

operator() также обычно перегружается для реализации функторов (или функциональных объектов), которые представляют собой классы, которые работают как функции. Преимущество функтора перед обычной функцией состоит в том, что функторы могут хранить данные в переменных-членах (поскольку они являются классами).

Вот простой функтор:

class Accumulator
{
private:
    int m_counter{ 0 };
 
public:
    int operator() (int i) { return (m_counter += i); }
};
 
int main()
{
    Accumulator acc{};
    std::cout << acc(10) << '\n'; // печатает 10
    std::cout << acc(20) << '\n'; // печатает 30
 
    return 0;
}

Обратите внимание, что использование нашего класса Accumulator выглядит так же, как обычный вызов функции, но наш объект Accumulator хранит накопленное значение.

Вы можете задаться вопросом, почему мы не можем сделать то же самое с обычной функцией и статической локальной переменной для сохранения данных между вызовами функций. Мы могли бы, но поскольку функции имеют только один глобальный экземпляр, мы могли бы использовать его только для одного объекта в какой-либо момент времени. С помощью функторов мы можем создать столько отдельных объектов-функторов, сколько нам нужно, и использовать их все одновременно.

Заключение

operator() иногда перегружается с двумя параметрами для индексации многомерных массивов или для получения подмножества одномерного массива (с двумя параметрами, определяющими возвращаемое подмножество). Всё остальное, вероятно, лучше написать как функцию-член с более описательным именем.

operator() также часто перегружается для создания функторов. Хотя простые функторы (такие как пример выше) довольно легко понять, функторы обычно используются в более сложных темах программирования и заслуживают отдельного урока.

Небольшой тест

Вопрос 1

Напишите класс, содержащий строку. Перегрузите operator(), чтобы возвращать подстроку, которая начинается с индекса, равного первому параметру. Длина подстроки должна определяться вторым параметром.

Подсказка: вы можете использовать индексы массива для доступа к отдельным символам в std::string.

Подсказка: вы можете использовать operator+=, чтобы добавить что-нибудь в строку.

Должен запуститься следующий код:

int main()
{
    Mystring string{ "Hello, world!" };
    std::cout << string(7, 5) << '\n'; // начинаем с индекса 7 и возвращаем 5 символов
 
    return 0;
}

Это должно напечатать

world

#include <string>
#include <iostream>
#include <cassert>
 
class Mystring
{
private:
    std::string m_string{};
 
public:
    Mystring(const std::string& string = {})
        :m_string{ string }
    {
    }
 
    std::string operator()(int start, int length)
    {
        assert(start >= 0);
        assert(start + length <= static_cast<int>(m_string.length()) &&
               "Mystring::operator(int, int): Substring is out of range");
 
        std::string ret{};
        for (int count{ 0 }; count < length; ++count)
        {
            ret += m_string[static_cast<std::string::size_type>(start + count)];
        }
        
        return ret;
    }
};
 
int main()
{
    Mystring string{ "Hello, world!" };
    std::cout << string(7, 5) << '\n'; // начинаем с индекса 7 и возвращаем 5 символов
 
    return 0;
}

Если вам нужна подстрока, используйте функцию std::string::substr.

Теги

C++ / CppLearnCppДля начинающихОбучениеОператор (программирование)Перегрузка (программирование)Перегрузка операторовПрограммированиеФунктор

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.