Почему С++ 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 при отбрасывании.

Надеюсь, это поможет!