10

Что такое функторы в C++ и где они применяются?

18

Я постоянно слышу много разговоров о функторях в C++. Можете ли вы дать общее представление о том, что это такое и в каких случаях они могут быть полезны?

5 ответ(ов)

12

Функтор — это, по сути, класс, который определяет оператор operator(). Это позволяет вам создавать объекты, которые "выглядят" как функции:

// это функтор
struct add_x {
  add_x(int val) : x(val) {}  // Конструктор
  int operator()(int y) const { return x + y; }

private:
  int x;
};

// Теперь вы можете использовать его так:
add_x add42(42); // создаем экземпляр класса функтора
int i = add42(8); // и "вызываем" его
assert(i == 50); // и он добавил 42 к своему аргументу

std::vector<int> in; // предположим, что здесь находится bunch of значений
std::vector<int> out(in.size());
// Передаем функтор в std::transform, который вызывает функтор для каждого элемента 
// во входной последовательности и сохраняет результат в выходную последовательность
std::transform(in.begin(), in.end(), out.begin(), add_x(1)); 
assert(out[i] == in[i] + 1); // для всех i

У функторов есть несколько приятных особенностей. Во-первых, в отличие от обычных функций, они могут содержать состояние. В примере выше создается функция, которая добавляет 42 к тому, что вы ей передаете. Но это значение 42 не зашито в код — оно задается как аргумент конструктора при создании экземпляра функтора. Я мог бы создать другой функтор, который добавляет 27, просто вызвав конструктор с другим значением. Это делает их приятно настраиваемыми.

Как показывают последние строки, вы часто передаете функтора в качестве аргументов другим функциям, таким как std::transform или другим алгоритмам стандартной библиотеки. Вы могли бы сделать то же самое с обычным указателем на функцию, но, как я уже говорил, функтора можно "настраивать", поскольку они содержат состояние, что делает их более гибкими (если бы я хотел использовать указатель на функцию, мне пришлось бы написать функцию, которая добавляет ровно 1 к своему аргументу. Функтор же универсален и добавляет то, с чем вы его инициализировали). Кроме того, они потенциально могут быть более эффективными. В приведенном примере компилятор точно знает, какую функцию должен вызвать std::transform. Он должен вызвать add_x::operator(). Это позволяет компилятору встроить этот вызов функции. И это делает его таким же эффективным, как если бы я вручную вызывал функцию для каждого значения в векторе.

Если бы я вместо этого передал указатель на функцию, компилятор не мог бы сразу увидеть, на какую функцию он указывает, поэтому, если он не выполнит довольно сложные глобальные оптимизации, он должен будет разыменовать указатель во время выполнения, а затем сделать вызов.

1

Функтор — это объект, который ведет себя как функция. По сути, это класс, который определяет оператор operator().

class MyFunctor
{
   public:
     int operator()(int x) { return x * 2; }
};

MyFunctor doubler;
int x = doubler(5); // x будет равно 10

Основное преимущество фунтора в том, что он может хранить состояние.

class Matcher
{
   int target;
   public:
     Matcher(int m) : target(m) {}
     bool operator()(int x) { return x == target; }
};

Matcher Is5(5);

if (Is5(n))    // то же самое, что if (n == 5)
{ 
    // здесь выполняется код, если n равно 5 
}

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

0

Как упоминали другие, функтор — это объект, который ведет себя как функция, т.е. перегружает оператор вызова функции.

Функторы часто используются в алгоритмах STL. Они полезны, поскольку могут хранить состояние перед и между вызовами функции, подобно замыканиям в функциональных языках. Например, вы можете определить функтор MultiplyBy, который умножает свой аргумент на заданное значение:

class MultiplyBy {
private:
    int factor;

public:
    MultiplyBy(int x) : factor(x) {
    }

    int operator () (int other) const {
        return factor * other;
    }
};

Затем вы можете передать объект MultiplyBy в алгоритм, например, std::transform:

int array[5] = {1, 2, 3, 4, 5};
std::transform(array, array + 5, array, MultiplyBy(3));
// Теперь массив выглядит так: {3, 6, 9, 12, 15}

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

0

Для новичков, подобных мне: после небольшого исследования я разобрался, что делал код, размещённый jalf.

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

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

Вот мой код (имена переменных у jalf показались мне запутанными):

class myFunctor { 
    public:
        /* myFunctor — это конструктор. parameterVar — параметр, переданный 
           в конструктор. : — это оператор инициализирующего списка. myObject — 
           закрытый член класса myFunctor. parameterVar передаётся 
           в оператор () и добавляется к myObject в перегруженной функции оператора () */
        myFunctor(int parameterVar) : myObject(parameterVar) {}

        /* слово "operator" — это ключевое слово, указывающее, что эта функция 
           является функцией перегруженного оператора. Следующие () просто 
           сообщают компилятору, что перегружается оператор (). После этого идёт
           параметр для перегруженного оператора. Этот параметр фактически 
           представляет собой аргумент "parameterVar", переданный мы только что 
           написанному конструктору. Последняя часть этого выражения 
           — тело перегруженного оператора, которое добавляет переданный 
           параметр к члену объекта. */
        int operator()(int myArgument) { return myObject + myArgument; }

    private: 
        int myObject; // Наш закрытый член объекта.
}; 

Если что-то из этого неточно или просто неправильно, не стесняйтесь меня исправить!

0

Вот реальная ситуация, в которой мне пришлось использовать Функтор для решения моей проблемы:

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

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

Но потом я осознал, что в каждом случае конкретная функция требует совершенно разного профиля параметров! Иногда это 2 параметра, иногда 5 параметров и так далее.

Другим решением могло бы быть создание базового класса, где конкретная функция является переопределённым методом в производном классе. Но действительно ли я хочу строить всю эту ИЕРАРХИЮ, только для того, чтобы передать указатель на функцию????

РЕШЕНИЕ: Итак, что я сделал — создал обертку (Функтор), которая может вызывать любую из нужных мне функций. Я настроил его заранее (с параметрами и т.д.) и затем передал его вместо указателя на функцию. Теперь вызываемый код может инициировать Функтор, не зная, что происходит внутри. Он даже может вызывать его несколько раз (мне нужно было, чтобы он вызывался 3 раза).


Вот и всё — практический пример, где Функтор оказался очевидным и простым решением, что позволило мне сократить дублирование кода с 20 функций до 1.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь