Появившиеся в C++11 лямбды стали одной из самых крутых фич нового стандарта языка, позволив сделать обобщённый код более простым и читабельным. Каждая новая версия стандарта C++ добавляет новые возможности лямбдам, делая обобщённый код ещё проще и читабельнее. Вы заметили, что слово «обобщённый» повторилось дважды? Это неспроста – лямбды действительно хорошо работают с кодом, построенным на шаблонах. Но при попытке использовать их в необобщённом, построенном на конкретных типах коде, мы сталкиваемся с рядом проблем. Статья о причинах и путях решения этих проблем. Для начала определимся с терминологией: лямбдой мы называем lambda-expression – это выражение C++, определяющее объект замыкания (closure object). Вот цитата из стандарта C++: A lambda-expression is a prvalue whose result object is called the closure object.[Note 1: A closure object behaves like a function object. — end note] Тип объекта замыкания – это уникальный безымянный класс. The type of a lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type, called the closure type, whose properties are described below. «Безымянный» в данном случае означает, что тип замыкания нельзя явно указать в коде, но получить его можно, чем мы ниже будем активно пользоваться. «Уникальный» означает, что каждая лямбда порождает новый тип замыкания, т. е. две абсолютно одинаковые с синтаксической точки зрения лямбды (будем называть такие лямбды однородными) имеют разные типы: Аналогично этот принцип распространяется и на обобщённый код, зависящий от типов замыканий: Это свойство не позволяет, например, складывать объекты замыканий в контейнеры (например, в std::vector<>). Стандарт языка предоставляет готовое решение этой проблемы в виде std::function<>. Действительно, объект std::function<> может оборачивать однородные лямбды: Такое решение действительно решает большинство проблем, однако подходит далеко не во всех случаях. Из объекта std::function<> нельзя получить сырой указатель на функцию, чтобы передать его, например, в какое-нибудь legacy API. Допустим, у нас есть функция: Интересно, что если мы попробуем передать в эту функцию любую из объявленных выше лямбд (l1 или l2), то код замечательно скомпилируется и запустится: Так получается потому, что лямбда с пустым замыканием (их ещё называют лямбды без состояния) по стандарту может быть неявно преобразована в указатель на функцию: The closure type for a non-generic lambda-expression with no lambda-capture whose constraints (if any) are satisfied has a conversion function to pointer to function with C++ language linkage having the same parameter and return types as the closure type's function call operator. The conversion is to “pointer to noexcept function” if the function call operator has a non-throwing exception specification. The value returned by this conversion function is the address of a function F that, when invoked, has the same effect as invoking the closure type's function call operator on a default-constructed instance of the closure type. F is a constexpr function if the function call operator is a constexpr function and is an immediate function if the function call operator is an immediate function. Для обобщённого кода можно применить явный static_cast<>: Есть простой синтаксический трюк, позволяющий не писать громоздкий static_cast<> и не указывать явно сигнатуру функции: Этот трюк работает из-за того, что унарный оператор + имеет встроенную перегрузку для любого типа. For every type T there exist candidate operator functions of the formT* operator+(T*); Такая перегрузка, применённая к объекту замыкания, вызывает неявное преобразование к указателю на функцию, что аналогично явному static_cast<>. Описанный выше трюк отлично работает для лямбд с пустыми замыканиями, но как быть, если нужно передать лямбду с состоянием? Классический приём из старого доброго C – передавать указатель на функцию и указатель на контекст типа void*. Функция получает этот указатель, преобразовывает его к указателю на нужный тип и получает доступ к контексту. Попробуем передать лямбду с состоянием в такую функцию: Здесь мы задаём новую лямбду, уже без состояния, которая неявно преобразуется в указатель на функцию. Эта лямбда получает контекст типа void*, преобразует его к указателю на тип замыкания, разыменовывает и вызывает как обычный функциональный объект. Кстати, это работает и с mutable лямбдами: Кажется, всё уже отлично работает. Но писать лямбду руками при каждом вызове api_func_ctx утомительно, хочется всё это обобщить и завернуть в красивую обёртку. Технически для того, чтобы сохранить лямбду с состоянием, а потом восстановить её, достаточно 2х объектов: контекст типа void* (это классический пример type erasure); указатель на функцию, принимающую контекст и все параметры лямбды и возвращающую такой же тип. Назовём тип для хранения «разобранного» объекта замыкания closure_erasure: Тут возникает проблема: как из типа замыкания выудить тип возвращаемого значения и параметров? На помощь нам приходит CTAD – этот тип может выводиться из типа параметра конструктора. Но что такого можно передать в конструктор от лямбды, из чего можно вывести все необходимые типы? Компилятор определяет для типа замыкания operator(), позволяющий вызывать объект замыкания как функциональный объект. Сигнатура этого оператора как раз и содержит всю необходимую информацию: Два конструктора отличаются только const в типе указателя на метод класса – это нужно для того, чтобы можно было оборачивать и обычные (константные), и mutable лямбды. Остался последний шаг: обернуть создание обёртки в удобную функцию, чтобы не нужно было вручную извлекать указатель на operator() из типа замыкания: Обратите внимание, что мы принимаем объект замыкания по неконстантной ссылке. Это важный момент, который намекает на важное ограничение применяемого механизма: мы несём ответственность за время жизни объекта замыкания! Если мы ещё вспомним, что функция и лямбда может быть ещё noexcept, то финальная версия обёртки будет выглядеть так: Compiler Explorer с кодом из статьи Книга про лямбды "C++ Lambda Story" Back to Basics: Lambdas from Scratch - Arthur O'Dwyer - CppCon 2019 C++ Weekly - Ep 246 - (+[](){})() What Does It Mean? Текущий черновик стандарта C++ Большое спасибо Валерию Артюхину за корректуру. Программист