Зачем переопределять оператор ()?
В библиотеке 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
}