Почему С++ 11 неявно конвертирует lambdas в объекты std:: function?
Я реализовал общий класс эмиттеров событий, который позволяет коду регистрировать обратные вызовы и испускать события с аргументами. Я использовал стирание типа Boost.Any для хранения обратных вызовов, чтобы они могли иметь произвольные сигнатуры параметров.
Все работает, но по какой-то причине lambdas передается, сначала должен быть превращен в объекты std::function
. Почему компилятор не делает вывод, что лямбда является типом функции? Это из-за того, как я использую вариативные шаблоны?
Я использую Clang (строка версии: Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn)
).
код:
#include <functional>
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include <boost/any.hpp>
using std::cout;
using std::endl;
using std::function;
using std::map;
using std::string;
using std::vector;
class emitter {
public:
template <typename... Args>
void on(string const& event_type, function<void (Args...)> const& f) {
_listeners[event_type].push_back(f);
}
template <typename... Args>
void emit(string const& event_type, Args... args) {
auto listeners = _listeners.find(event_type);
for (auto l : listeners->second) {
auto lf = boost::any_cast<function<void (Args...)>>(l);
lf(args...);
}
}
private:
map<string, vector<boost::any>> _listeners;
};
int main(int argc, char** argv) {
emitter e;
int capture = 6;
// Not sure why Clang (at least) can't deduce the type of the lambda. I don't
// think the explicit function<...> business should be necessary.
e.on("my event",
function<void ()>( // <--- why is this necessary?
[&] () {
cout << "my event occurred " << capture << endl;
}));
e.on("my event 2",
function<void (int)>(
[&] (int x) {
cout << "my event 2 occurred: " << x << endl;
}));
e.on("my event 3",
function<void (double)>(
[&] (double x) {
cout << "my event 3 occurred: " << x << endl;
}));
e.on("my event 4",
function<void (int, double)>(
[&] (int x, double y) {
cout << "my event 4 occurred: " << x << " " << y << endl;
}));
e.emit("my event");
e.emit("my event 2", 1);
e.emit("my event 3", 3.14159);
e.emit("my event 4", 10, 3.14159);
return EXIT_SUCCESS;
}
Ответы
Ответ 1
Лямбда не является std::function
, а std::function
не является лямбдой.
Лямбда - синтаксический сахар для создания анонимного класса, который выглядит следующим образом:
struct my_lambda {
private:
int captured_int;
double captured_double;
char& referenced_char;
public:
int operator()( float passed_float ) const {
// code
}
};
int captured_int = 7;
double captured_double = 3.14;
char referenced_char = 'a';
my_lambda closure {captured_int, captured_double, referenced_char};
closure( 2.7f );
из этого:
int captured_int = 7;
double captured_double = 3.14;
char referenced_char = 'a';
auto closure = [=,&referenced_char](float passed_float)->int {
// code
};
closure(2.7);
с именем типа my_lambda
, фактически являющимся некорректным типом.
A std::function
- совершенно другая вещь. Это объект, который реализует operator()
с определенной сигнатурой и сохраняет указатель умственного значения-семантики абстрактного интерфейса, который охватывает операции копирования/перемещения/вызова. Он имеет конструктор template
d, который может использовать любой тип, поддерживающий copy/move/operator()
с совместимой сигнатурой, генерирует конкретный пользовательский класс, реализующий абстрактный внутренний интерфейс, и сохраняет его в вышеупомянутой внутренней семантике значений умный указатель.
Затем он переводит операции из себя как типа значения в абстрактный внутренний указатель, включая совершенную переадресацию методу вызова.
Как это бывает, вы можете сохранить лямбда в std::function
, точно так же, как вы можете сохранить указатель на функцию.
Но существует целый ряд разных std::function
, которые могли бы хранить данный лямбда - все, где типы конвертируются в аргументы и из них, и на самом деле работает одинаково хорошо, поскольку std::function
заинтересованных сторон.
Вывод типа в С++ в template
не работает на уровне "вы можете преобразовать в" - это соответствие шаблонов, чистое и простое. Поскольку лямбда является типом, не связанным с любым std::function
, из него может быть выведен тип std::function
.
Если С++ попытался сделать это в общем случае, ему пришлось бы инвертировать процесс Turing-complete для определения того, что (если есть) набор типов может быть передан в template
, чтобы создать совместимый с преобразованием экземпляр.
В теории мы могли бы добавить "оператор выводить аргументы шаблона из" на язык, где разработчики данного template
могут писать код, который принимает некоторый произвольный тип, и они пытаются дразнить "из этого типа, что Параметры template
должны использоваться для экземпляра". Но С++ этого не делает.
Ответ 2
Компилятор ничего не делает, потому что компилятор реализует язык С++, а правила вывода аргументов шаблона шаблона языка не позволяют вычесть так, как вы хотите.
Вот простой пример, который представляет вашу ситуацию:
template <typename T> struct Foo
{
Foo(int) {}
};
template <typename T> void magic(Foo<T> const &);
int main()
{
magic(10); // what is T?
}
Ответ 3
Когда boost::any
сохраняет значение, он использует статический тип этого объекта для определения того, какой тип объекта хранится. Затем вы можете вернуть any
к объекту правильного типа, если вы укажете статический тип того, что хранится.
Каждая С++ лямбда связана с определенным по реализации типом, непрозрачным для пользователя. Хотя лямбды можно назвать функциями, они не оценивают непосредственно std::function
s. Приведение происходит при хранении лямбда в any
, чтобы гарантировать, что статический тип сохраненного значения std::function
при отбрасывании.
Надеюсь, это поможет!