Зачем переопределять оператор ()?

В библиотеке Boost Signals они перегружают оператор ().

Это соглашение в C++? Для обратных вызовов и т. Д.?

Я видел это в коде сотрудника (который является большим фанатом Boost). Из всего Буста, это привело меня в замешательство.

Любое понимание причины этой перегрузки?

12 ответов

Решение

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

Вот простой пример функтора:

struct Accumulator
{
    int counter = 0;
    int operator()(int i) { return counter += i; }
}
...
Accumulator acc;
cout << acc(10) << endl; //prints "10"
cout << acc(20) << endl; //prints "30"

Функторы широко используются в общем программировании. Многие алгоритмы STL написаны в очень общем виде, так что вы можете подключить свою собственную функцию / функтор в алгоритм. Например, алгоритм std::for_each позволяет применить операцию к каждому элементу диапазона. Это может быть реализовано что-то вроде этого:

template <typename InputIterator, typename Functor>
void for_each(InputIterator first, InputIterator last, Functor f)
{
    while (first != last) f(*first++);
}

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

void print(int i) { std::cout << i << std::endl; }
...    
std::vector<int> vec;
// Fill vec

// Using a functor
Accumulator acc;
std::for_each(vec.begin(), vec.end(), acc);
// acc.counter contains the sum of all elements of the vector

// Using a function pointer
std::for_each(vec.begin(), vec.end(), print); // prints all elements

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

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

так как то так:

logger.log("Log this message");

превращается в это:

logger("Log this message");

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

Ответ в том, что функтор может иметь состояние. Рассмотрим функцию суммирования - она ​​должна содержать промежуточную сумму.

class Sum
{
public:
    Sum() : m_total(0)
    {
    }
    void operator()(int value)
    {
        m_total += value;
    }
    int m_total;
};

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

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

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

Вы также можете посмотреть пример матрицы C++ faq. Есть хорошие способы сделать это, но это, конечно, зависит от того, чего вы пытаетесь достичь.

Начните использовать std::for_each, std::find_ifи т. д. чаще в вашем коде, и вы поймете, почему удобно иметь возможность перегружать оператор (). Это также позволяет функторам и задачам иметь понятный вызывающий метод, который не будет конфликтовать с именами других методов в производных классах.

Одна сильная сторона, которую я могу видеть, однако это можно обсудить, заключается в том, что сигнатура operator() выглядит и ведет себя одинаково для разных типов. Если бы у нас был класс Reporter, у которого был отчет по методу-члену (..), а затем другой класс Writer, у которого был метод-член write(..), нам пришлось бы писать адаптеры, если бы мы хотели использовать оба класса как возможно компонент шаблона некоторой другой системы. Все, что его волнует, - это передавать строки или что-то еще. Без использования оператора () с перегрузкой или написанием адаптеров специального типа вы не могли бы делать такие вещи, как

T t;
t.write("Hello world");

потому что T требует, чтобы была функция-член write, которая принимает что-либо неявно преобразуемое в const char* (или, скорее, const char[]). Класс Reporter в этом примере не имеет этого, поэтому T (параметр шаблона), являющийся Reporter, не сможет скомпилироваться.

Тем не менее, насколько я вижу, это будет работать с различными типами

T t;
t("Hello world");

тем не менее, он все еще явно требует, чтобы у типа T был определен такой оператор, поэтому у нас все еще есть требование к T. Лично я не думаю, что это слишком странно с функторами, поскольку они обычно используются, но я бы предпочел увидеть другие механизмы для это поведение. В таких языках, как C#, вы можете просто передать делегат. Я не слишком знаком с указателями на функции-члены в C++, но могу предположить, что вы могли бы добиться такого же поведения и там.

Кроме поведения синтетического сахара, я не вижу сильных сторон перегрузки оператора для выполнения таких задач.

Я уверен, что есть более сознательные люди, у которых есть более веские причины, чем у меня, но я подумал, что выложу свое мнение, чтобы поделиться с вами остальными.

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

Другой сотрудник отметил, что это может быть способ замаскировать функторные объекты как функции. Например, это:

my_functor();

Это действительно:

my_functor.operator()();

Так значит ли это:

my_functor(int n, float f){ ... };

Может быть использован и для перегрузки?

my_functor.operator()(int n, float f){ ... };

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

Недавно я использовал некоторый код, который очень широко использует operator(). Недостатком перегрузки этого оператора является то, что в результате некоторые IDE становятся менее эффективными инструментами. В Visual Studio обычно можно щелкнуть правой кнопкой мыши вызов метода, чтобы перейти к определению метода и / или объявлению. К сожалению, VS не достаточно умен, чтобы индексировать вызовы operator(). Особенно в сложном коде с переопределенными определениями operator() повсюду, может быть очень трудно выяснить, какой кусок кода и где выполняется. В некоторых случаях мне приходилось запускать код и прослеживать его, чтобы найти то, что на самом деле работает.

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

Легко запутаться между Functorа также user-defined conversion function.

Ниже 2 примера показывают разницу между

      1. Functor
2. User-defined conversion function

1. Функтор:

         struct A {
     int t = 0;
     int operator()(int i) { return t += i; } // must have return type or void
   };

   int main() {
     A a;
     cout << a(3); // 3
     cout << a(4); // 7 (Not 4 bcos it maintaines state!!!)
   }

2. Пользовательская функция преобразования:

          struct A {
        int t = 3;
        operator int() { return t; } // user-defined conversion function 
                                     // Return type is NOT needed (incl. void)
    };

    int main() {
        cout << A(); // 3 - converts the object{i:3} into integer 3

        A a;
        cout << a;   // 3 - converts the object{i:3} into integer 3
    }
Другие вопросы по тегам