Функциональные объекты в C++ (C++11)

Я читаю о boost::function и немного запутался в его использовании и его отношении к другим конструкциям или терминам С++, которые я нашел в документации, например. здесь.

В контексте C++ (C++11) в чем разница между экземпляром boost::function, функциональным объектом, функтором и лямбда-выражением ? Когда какую конструкцию следует использовать? Например, когда я должен обернуть объект функции в boost::function вместо того, чтобы использовать объект напрямую?

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


person Giorgio    schedule 10.10.2012    source источник


Ответы (3)


Функциональный объект и функтор — одно и то же; объект, реализующий оператор вызова функции operator(). Лямбда-выражение создает объект функции. Объекты с типом некоторой специализации boost::function/std::function также являются функциональными объектами.

Lambda отличается тем, что лямбда-выражения имеют анонимный и уникальный тип и являются удобным способом создания встроенного функтора.

boost::function/std::function отличается тем, что превращает любую вызываемую сущность в функтор, тип которого зависит только от сигнатуры вызываемой сущности. Например, каждое лямбда-выражение имеет уникальный тип, поэтому их трудно передать в неуниверсальном коде. Если вы создаете std::function из лямбды, вы можете легко передать обернутую лямбду.

person bames53    schedule 10.10.2012
comment
Итак, boost::function используется для определения подписи функционального объекта? - person Giorgio; 10.10.2012
comment
@Giorgio Подпись, но не ее тип. - person pmr; 10.10.2012
comment
@ Джорджио Не для определения подписи, а для скрытия всего, кроме подписи. Это также удобно позволяет обрабатывать необъектные вызываемые сущности как объекты. - person bames53; 10.10.2012
comment
Например, каждое лямбда-выражение имеет уникальный тип: означает ли это, что два лямбда-выражения с одинаковой сигнатурой (скажем, (int x, int y) -> int) имеют разные типы? - person Giorgio; 10.10.2012
comment
@Giorgio Не только это, но и тип уникален, даже если точная последовательность токенов в двух лямбда-выражениях идентична: static_assert(!std::is_same<decltype([]{}),decltype([]{})>::value,"these two lambda expressions have different types!"); - person bames53; 10.10.2012
comment
@ bames53: Я полагаю, что под капотом создаются два класса функторов, которые создаются на лету. Интересно, можно ли использовать общий суперкласс вместо boost::function, но я думаю, что это невозможно, потому что эти классы с одним экземпляром анонимны. Однако это должно быть возможно для обычных функторов (не анонимных). - person Giorgio; 10.10.2012
comment
@Giorgio Этот общий суперкласс - это именно то, чем является std::function. Но это не принудительно, потому что вам не нужно использовать типы с одним и тем же интерфейсом, чтобы иметь общую базу с шаблонами. - person pmr; 10.10.2012

И boost::function, и стандартная версия std::function являются оболочками, предоставляемыми библиотекой. Они потенциально дороги и довольно громоздки, и их следует использовать только в том случае, если вам действительно нужна коллекция разнородных вызываемых сущностей. Пока вам нужен только один вызываемый объект за раз, вам гораздо лучше использовать auto или шаблоны.

Вот пример:

std::vector<std::function<int(int, int)>> v;

v.push_back(some_free_function);           // free function
v.push_back(&Foo::mem_fun, &x, _1, _2);    // member function bound to an object
v.push_back([&](int a, int b) -> int { return a + m[b]; });  // closure

int res = 0;
for (auto & f : v) { res += f(1, 2); }

Вот контрпример:

template <typename F>
int apply(F && f)
{
    return std::forward<F>(f)(1, 2);
}

В этом случае было бы совершенно бесполезно объявлять apply следующим образом:

int apply(std::function<int(int,int)>)   // wasteful

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

person Kerrek SB    schedule 10.10.2012

Функциональные объекты и функторы часто описываются в терминах концепции. Это означает, что они описывают набор требований типа. Многое в отношении функторов изменилось в C++11, и новая концепция называется Callable. Объект o вызываемого типа — это объект, в котором (по существу) выражение o(ARGS) истинно. Примеры для Callable:

int f() {return 23;}

struct FO {
  int operator()() const {return 23;}
};

Часто также добавляются некоторые требования к возвращаемому типу Callable. Вы используете Callable следующим образом:

template<typename Callable>
int call(Callable c) {
  return c();
}

call(&f);
call(FO());

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

std::function - это такой же Callable, но он позволяет вам стереть фактический тип, который вы вызываете (например, ваша функция, принимающая вызываемый объект, больше не является шаблоном). Тем не менее, вызов функции требует, чтобы вы знали ее аргументы и тип возвращаемого значения, поэтому они должны быть указаны как аргументы шаблона для std::function.

Вы бы использовали это так:

int call(std::function<int()> c) {
  return c();
}

call(&f);
call(FO());

Вы должны помнить, что использование std::function может повлиять на производительность, и вы должны использовать его только тогда, когда уверены, что вам это нужно. Почти во всех других случаях шаблон решает вашу проблему.

person pmr    schedule 10.10.2012