Шаблоны с переменным количеством аргументов на примере обертки для Lua

от автора

Понадобилось мне прикрутить Lua к проекту на C++. Писать обертки в ручную — лень (слишком много писать), готовые не подходили по тем или иным причинам. Решил написать свою. А потому задался вопросом, как максимально упростить интерфейс? От одной только мысли об этом в голову лезли жутчайшие конструкции из шаблонов. Так оно в последствии и оказалось, но гораздо проще, чем представлялось.

В C++11 появились шаблоны с переменным числом аргументов, это позволяет писать шаблонные функции/классы так, как в C++03 было невозможно вовсе. Такие шаблоны сильно упрощают задачу.

Первым делом понадобилось написать обертку над простейшими действиями с интерпретатором (можно было бы обойтись простыми вызовами к C API Lua, но держать в памяти кучу индексов различных значений в стеке мне не хочется. Поэтому я обернул их в несколько функций, которые помимо того, что избавляют от необходимости передавать в каждую функцию указатель на состояние интерпретатора, практически не требуют индексов, так как они имеют значения по умолчанию.

В итоге хотелось увидеть интерфейс близкий к следующему:

lua.export_function(some_function); 


Можно попробовать. Однако интерфейс будет все-таки чуточку сложнее. Нужно указать интерпретатору имя для экспортируемой функции. И передавать будем адрес на функцию.

lua.export_function("some_function", &some_function); 

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

  • Возвращаемым значением колбэка:
    template <typename T> void some_function(T (*callback)()) {} 

  • Параметром колбэка:
    template <typename T> void some_function(void (*callback)(T)) {} 

  • Классом, которому принадлежит метод.
    template <typename T> void some_function(void (T::*method)()) {} 

Все эти случаи (и еще несколько других), могут комбинироваться. Можно этим воспользоваться.

template <typename R, typename... Args> void export_function(const std::string& name, T (*function)(Args...)) { } 

Теперь, можно взяться за собственно экспорт функции. Для каждой функции создадим лямбду, которая будет принимать аргументы от интерпретатора, передавать их в функцию, а потом, возвращать интерпретатору результат. Лямбда должна храниться всё время, что работает экземпляр интерпретатора, поэтому указатель на каждую лямбду я сохраняю внутри класса и удаляю в деструкторе.

template <typename R, typename... Args> void export_function(const std::string& name, T (*function)(Args...)) {     auto function = new std::function<int(Lua&)>([function](Lua& vm) -> int {         auto tuple = args<Args...>();         return apply_function<std::tuple_size<decltype(tuple)>::value>             ::apply(function, tuple);     });     lambda(function); } 

Выглядит странно. Попробуем разобраться. Для начала надо получить все аргументы от интерпретатора.

template <typename T, typename T1, typename... Args> std::tuple<T, T1, Args...> args(const int i = 1) {     T t = arg<T>(i);     return std::tuple_cat(t, args<T1, Args...>(i+1)); } 

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

template <typename T> std::tuple<T> args(const int i = 1) {     return std::tuple<T>(arg<T>(i)); } 

Функция arg — очевидна, не буду её приводить, всё что требуется — написать несколько специализаций.
Теперь, когда у нас есть все аргументы в одном кортеже, надо передать их все в функцию.

template <int N> struct apply_function {     template <typename R, typename... FunctionArgs, typename... TupleArgs,         typename... Args>     static R apply(R (*function)(Args...), std::tuple<TupleArgs...>,         Args... args) {         return apply_function<N-1>::apply(function, tuple, std::get<N-1>::value, args);     } }; 

И нужно специализировать этот шаблон для последней итерации.

template <> struct apply_function<0> {     template <typename R, typename... FunctionArgs, typename... TupleArgs,         typename... Args>     static R apply(R (*function)(Args...), std::tuple<TupleArgs...>,         Args... args) {         return (*function)(args...);     } }; 

Помимо всего этого потребуется еще несколько специализаций (проблемы в типе void).

Результаты

Получилась вполне рабочая обертка для экспорта C++ функций и классов в lua. Из очевидных минусов вижу всего несколько:

  • Лямбды все-таки медленнее колбэков, при желании можно переписать код без них, но получится больше шаблонных функций.
  • При каждом вызове функции/метода мы получаем две рекурсии, глубина которых равна количеству аргументов функций. Возможно компилятор сделает всю эту орду шаблонных функций инлайновыми, я не проверял (и не уверен в этом).
  • Шаблоны сильно сказываются на времени компиляции. Но даже на моем довольно слабом ноутбуке сборка этой обертки и кода, который её использует, занимает гораздо меньше времени, чем сборка кода, который использует boost, так что это не критично.
  • Нет поддержки множественного наследования — слишком муторно его делать.
  • Нет доступа к метатаблицам, а значит нет переопределения операторов.
  • Нет поддержки перегрузки функций, но можно просто дать перегружаемым функциям разные имена.

Последние три пункта, возможно, сделаю чуть позже.
А теперь плюсы:

  • Простой интерфейс.
  • Решение на чистом C++11, не требует генерации дополнительного кода дополнительными инструментами.

Как использовать

Прежде всего нужно создать объект класса util::Lua, при этом проинициализируется интерпретатор.

util::Lua vm; 

После этого можно экспортировать функции/классы.

Функции

Всё просто. Мы используем только указатель на функцию и имя, под которым она будет доступна в lua.

some_function();  vm.export_function("some_function", &some_function); 

Типы всех параметров и возвращаемого значения будут определены и обработаны корректно.

Классы

Экспортируемый класс надо подготовить. Для начала нужно унаследовать его от util::LuaClass, чтобы при возвращении объекта интерпретатору был возвращен именно объект, а не userdata. После нужно определить три статических метода.

  • Метод export_class должен экспортировать все методы/функции класса.
  • Метод export_me должен вызывать функцию Lua::export_class<A, B>()
  • Метод class_name должен возвращать имя класса.

class A : public util::LuaClass { public:     static void export_class(Lua& vm);     static void export_me(Lua& vm);     static const std::string class_name(); };  void A::export_me(Lua& vm) {     vm.export_class<A>(); }  class B: public A { public:     static void export_class(Lua& vm);     static void export_me(Lua& vm);     static const std::string class_name(); };  void B::export_me(Lua& vm) {     vm.export_class<B, A>(); } 

Функции util::Lua::export_class передаются в качестве параметров шаблона — класс, который мы хотим
экспортировать и его родитель, чтобы экспортировать и его (если это еще не сделано).

Самое интересное творится в методе export_class. К примеру:

vm.export_constructor<A, int>(); vm.export_function("static_method", &A::static_method); vm.export_method("method", &A::method); 

Всё просто. Статические методы экспортируем как функции, методы — похожим образом, но через отдельную функцию. Конструктор экспортируется как функция с именем new, типы его аргументов необходимо указать явно в качестве
аргументов шаблона, связано это с тем, что на конструктор нельзя взять указатель. Приятная вещь в том, что объекты созданные посредством вызова такого конструктора из lua будет обрабатывать Garbage Collector. Когда все ссылки на объект будут удалены будет вызван delete для объекта C++.

Код

Весь код выложен на гитхабе github.com/alex-ac/LuaCxx/ под MIT лицензией.
Буду рад увидеть комментарии, советы, фичреквесты и багрепорты.

ссылка на оригинал статьи http://habrahabr.ru/post/165765/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *