Что такое функторы C ++ и их использование? - PullRequest
792 голосов
/ 10 декабря 2008

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

Ответы [ 15 ]

956 голосов
/ 10 декабря 2008

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

// this is a functor
struct add_x {
  add_x(int x) : x(x) {}
  int operator()(int y) const { return x + y; }

private:
  int x;
};

// Now you can use it like this:
add_x add42(42); // create an instance of the functor class
int i = add42(8); // and "call" it
assert(i == 50); // and it added 42 to its argument

std::vector<int> in; // assume this contains a bunch of values)
std::vector<int> out(in.size());
// Pass a functor to std::transform, which calls the functor on every element 
// in the input sequence, and stores the result to the output sequence
std::transform(in.begin(), in.end(), out.begin(), add_x(1)); 
assert(out[i] == in[i] + 1); // for all i

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

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

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

117 голосов
/ 10 декабря 2008

Маленькое дополнение. Вы можете использовать boost::function для создания функторов из функций и методов, например:

class Foo
{
public:
    void operator () (int i) { printf("Foo %d", i); }
};
void Bar(int i) { printf("Bar %d", i); }
Foo foo;
boost::function<void (int)> f(foo);//wrap functor
f(1);//prints "Foo 1"
boost::function<void (int)> b(&Bar);//wrap normal function
b(1);//prints "Bar 1"

и вы можете использовать boost :: bind для добавления состояния в этот функтор

boost::function<void ()> f1 = boost::bind(foo, 2);
f1();//no more argument, function argument stored in f1
//and this print "Foo 2" (:
//and normal function
boost::function<void ()> b1 = boost::bind(&Bar, 2);
b1();// print "Bar 2"

и самое полезное, с boost :: bind и boost :: function вы можете создать функтор из метода класса, на самом деле это делегат:

class SomeClass
{
    std::string state_;
public:
    SomeClass(const char* s) : state_(s) {}

    void method( std::string param )
    {
        std::cout << state_ << param << std::endl;
    }
};
SomeClass *inst = new SomeClass("Hi, i am ");
boost::function< void (std::string) > callback;
callback = boost::bind(&SomeClass::method, inst, _1);//create delegate
//_1 is a placeholder it holds plase for parameter
callback("useless");//prints "Hi, i am useless"

Вы можете создать список или вектор функторов

std::list< boost::function<void (EventArg e)> > events;
//add some events
....
//call them
std::for_each(
        events.begin(), events.end(), 
        boost::bind( boost::apply<void>(), _1, e));

Существует одна проблема со всем этим, сообщения об ошибках компилятора не читаются человеком:)

89 голосов
/ 10 декабря 2008

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

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

MyFunctor doubler;
int x = doubler(5);

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

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

Matcher Is5(5);

if (Is5(n))    // same as if (n == 5)
{ ....}
45 голосов
/ 21 ноября 2009

Имя «функтор» традиционно использовалось в теории категорий задолго до появления C ++. Это не имеет ничего общего с C ++ концепцией функтора. Лучше использовать имя объект функции вместо того, что мы называем «функтором» в C ++. Так другие языки программирования называют подобные конструкции.

Используется вместо простой функции:

Особенности:

  • Объект функции может иметь состояние
  • Объект функции вписывается в ООП (он ведет себя как любой другой объект).

Минусы:

  • Приносит больше сложности в программу.

Используется вместо указателя функции:

Особенности:

  • Функциональный объект часто может быть встроенным

Минусы:

  • Функциональный объект не может быть заменен другим типом функционального объекта во время выполнения (по крайней мере, если он не расширяет некоторый базовый класс, который, следовательно, дает некоторые накладные расходы)

Используется вместо виртуальной функции:

Особенности:

  • Функциональный объект (не виртуальный) не требует vtable и диспетчерской диспетчеризации, поэтому в большинстве случаев он более эффективен

Минусы:

  • Функциональный объект не может быть заменен другим типом функционального объекта во время выполнения (по крайней мере, если он не расширяет некоторый базовый класс, что, следовательно, приводит к дополнительным расходам)
36 голосов
/ 10 декабря 2008

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

Функторы обычно используются в алгоритмах 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));
// Now, array is {3, 6, 9, 12, 15}

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

34 голосов
/ 05 января 2013

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

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

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

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

class myFunctor
{ 
    public:
        /* myFunctor is the constructor. parameterVar is the parameter passed to
           the constructor. : is the initializer list operator. myObject is the
           private member object of the myFunctor class. parameterVar is passed
           to the () operator which takes it and adds it to myObject in the
           overloaded () operator function. */
        myFunctor (int parameterVar) : myObject( parameterVar ) {}

        /* the "operator" word is a keyword which indicates this function is an 
           overloaded operator function. The () following this just tells the
           compiler that () is the operator being overloaded. Following that is
           the parameter for the overloaded operator. This parameter is actually
           the argument "parameterVar" passed by the constructor we just wrote.
           The last part of this statement is the overloaded operators body
           which adds the parameter passed to the member object. */
        int operator() (int myArgument) { return myObject + myArgument; }

    private: 
        int myObject; //Our private member object.
}; 

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

18 голосов
/ 06 мая 2013

Функтор - это функция высшего порядка , которая применяет функцию к параметризованным (то есть шаблонным) типам. Это обобщение функции map высшего порядка. Например, мы могли бы определить функтор для std::vector следующим образом:

template<class F, class T, class U=decltype(std::declval<F>()(std::declval<T>()))>
std::vector<U> fmap(F f, const std::vector<T>& vec)
{
    std::vector<U> result;
    std::transform(vec.begin(), vec.end(), std::back_inserter(result), f);
    return result;
}

Эта функция принимает std::vector<T> и возвращает std::vector<U>, когда ей передана функция F, которая принимает T и возвращает U. Функтор не обязательно должен быть определен для типов контейнеров, он также может быть определен для любого шаблонного типа, включая std::shared_ptr:

template<class F, class T, class U=decltype(std::declval<F>()(std::declval<T>()))>
std::shared_ptr<U> fmap(F f, const std::shared_ptr<T>& p)
{
    if (p == nullptr) return nullptr;
    else return std::shared_ptr<U>(new U(f(*p)));
}

Вот простой пример, который преобразует тип в double:

double to_double(int x)
{
    return x;
}

std::shared_ptr<int> i(new int(3));
std::shared_ptr<double> d = fmap(to_double, i);

std::vector<int> is = { 1, 2, 3 };
std::vector<double> ds = fmap(to_double, is);

Есть два закона, которым должны следовать функторы. Первый - это закон тождества, который гласит, что если функтору задана функция тождества, он должен быть таким же, как применение функции тождества к типу, то есть fmap(identity, x) должно совпадать с identity(x):

struct identity_f
{
    template<class T>
    T operator()(T x) const
    {
        return x;
    }
};
identity_f identity = {};

std::vector<int> is = { 1, 2, 3 };
// These two statements should be equivalent.
// is1 should equal is2
std::vector<int> is1 = fmap(identity, is);
std::vector<int> is2 = identity(is);

Следующим законом является закон композиции, который гласит, что если функтору дана композиция из двух функций, он должен быть таким же, как применение функтора для первой функции, а затем снова для второй функции. Итак, fmap(std::bind(f, std::bind(g, _1)), x) должно совпадать с fmap(f, fmap(g, x)):

double to_double(int x)
{
    return x;
}

struct foo
{
    double x;
};

foo to_foo(double x)
{
    foo r;
    r.x = x;
    return r;
}

std::vector<int> is = { 1, 2, 3 };
// These two statements should be equivalent.
// is1 should equal is2
std::vector<foo> is1 = fmap(std::bind(to_foo, std::bind(to_double, _1)), is);
std::vector<foo> is2 = fmap(to_foo, fmap(to_double, is));
9 голосов
/ 26 декабря 2011

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

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

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

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

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

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


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

3 голосов
/ 07 марта 2012

За исключением использования в обратном вызове, функторы C ++ могут также помочь обеспечить стиль доступа Matlab , подобный классу matrix . Есть пример .

2 голосов
/ 15 января 2017

Большим преимуществом реализации функций в качестве функторов является то, что они могут поддерживать и повторно использовать состояние между вызовами. Например, многие алгоритмы динамического программирования, такие как алгоритм Вагнера-Фишера для вычисления расстояния Левенштейна между строками, работают, заполняя большую таблицу результатов. Распределять эту таблицу при каждом вызове функции очень неэффективно, поэтому реализация функции в качестве функтора и превращение таблицы в переменную-член может значительно повысить производительность.

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

#include <string>
#include <vector>
#include <algorithm>

template <typename T>
T min3(const T& a, const T& b, const T& c)
{
   return std::min(std::min(a, b), c);
}

class levenshtein_distance 
{
    mutable std::vector<std::vector<unsigned int> > matrix_;

public:
    explicit levenshtein_distance(size_t initial_size = 8)
        : matrix_(initial_size, std::vector<unsigned int>(initial_size))
    {
    }

    unsigned int operator()(const std::string& s, const std::string& t) const
    {
        const size_t m = s.size();
        const size_t n = t.size();
        // The distance between a string and the empty string is the string's length
        if (m == 0) {
            return n;
        }
        if (n == 0) {
            return m;
        }
        // Size the matrix as necessary
        if (matrix_.size() < m + 1) {
            matrix_.resize(m + 1, matrix_[0]);
        }
        if (matrix_[0].size() < n + 1) {
            for (auto& mat : matrix_) {
                mat.resize(n + 1);
            }
        }
        // The top row and left column are prefixes that can be reached by
        // insertions and deletions alone
        unsigned int i, j;
        for (i = 1;  i <= m; ++i) {
            matrix_[i][0] = i;
        }
        for (j = 1; j <= n; ++j) {
            matrix_[0][j] = j;
        }
        // Fill in the rest of the matrix
        for (j = 1; j <= n; ++j) {
            for (i = 1; i <= m; ++i) {
                unsigned int substitution_cost = s[i - 1] == t[j - 1] ? 0 : 1;
                matrix_[i][j] =
                    min3(matrix_[i - 1][j] + 1,                 // Deletion
                    matrix_[i][j - 1] + 1,                      // Insertion
                    matrix_[i - 1][j - 1] + substitution_cost); // Substitution
            }
        }
        return matrix_[m][n];
    }
};
...