Зачем переопределять оператор ()? - PullRequest
47 голосов
/ 25 ноября 2008

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

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

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

Есть какое-нибудь понимание причины этой перегрузки?

Ответы [ 11 ]

130 голосов
/ 25 ноября 2008

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

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

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

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

22 голосов
/ 25 ноября 2008

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

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

logger.log("Log this message");

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

logger("Log this message");
5 голосов
/ 25 ноября 2008

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

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

class Sum
{
public:
    Sum() : m_total(0)
    {
    }
    void operator()(int value)
    {
        m_total += value;
    }
    int m_total;
};
4 голосов
/ 25 ноября 2008

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

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

3 голосов
/ 25 ноября 2008

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

2 голосов
/ 25 ноября 2008

Одна сильная сторона, которую я могу видеть, однако это можно обсудить, заключается в том, что сигнатура 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 ++, но могу предположить, что вы можете добиться такого же поведения и там.

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

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

2 голосов
/ 25 ноября 2008

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

2 голосов
/ 25 ноября 2008

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

2 голосов
/ 25 ноября 2008

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

1 голос
/ 25 ноября 2008

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

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

...