• Любопытно повторяющийся Python

В этом посте я попытаюсь объяснить идиому C++ CRTP и то, как связать ее с python с помощью pybind11.

Удивительно Rповторяющийся Template Pшаблон или CRTP — это идиома языка C++. Это тип статического или времени компиляции полиморфизма. Истинная сила CRTP заключается в том, что он разрешает все вызовы виртуальных функций во время компиляции, что позволяет избежать дополнительной памяти vPtr, vTable, а также избежать дополнительного разыменования указателя.

Если виртуальная подпрограмма вызывается много раз (порядка сотен тысяч), она снижает производительность системы, причина в том, что каждый раз, когда вызывается подпрограмма, ее адрес необходимо разрешить, просматривая vTable с помощью vPtr. Дополнительная косвенность (разыменование указателя) для каждого вызова виртуального метода делает доступ к vTable дорогостоящей операцией, а CRTP может разрешать все эти вызовы методов во время компиляции, что также позволяет избежать этих дорогостоящих операций и оказывает значительное влияние на производительность.

Теперь мы увидим пример CRTP.

Выше приведен простой пример использования CRTP. Здесь вы можете видеть, что я создал файл GenericParser. Этот Generic Parser является классом шаблона и имеет 3 метода: inorderParser, callAppropriate и закрытую функцию processNode. Парсер по порядку будет анализировать узлы дерева по порядку.

processNode — это функция, которая выполняет обработку на узлах, в данном случае печатая сообщение. В функции callAppropriate происходит волшебство. Этот метод статически приведет указатель this к типу шаблона, указанному во время создания объекта, и вызовет processNode приведенного объекта.

Теперь вот сложная часть. EmployeeParser является общедоступным производным от GenericParser, а GenericParser является классом шаблона с типом шаблона EmployeeParser.

В основной функции мы создаем объект EmployeeParser и вызываем функцию inorderParser. inorderParser является функцией GenericParser. Он вызовет функцию callAppropriate. callAppropriate преобразует объект this в тип T и вызывает функцию T processNode. В этом примере тип T на самом деле EmployeeParser, поэтому в конечном итоге была вызвана функция EmployeeParser's processNode. Таким образом достигается полиморфизм. Увидеть код в действии.

Теперь, когда мы знаем, что такое CRTP, мы можем перейти к основной теме этого блога, а именно к привязкам Python к CRTP. Чтобы связать код C++ с python, я сосредоточусь на pybind11.

Итак, дело в том, что вы не можете связать C++ CRTP с python «статически», потому что CRTP зависит от типа шаблона, о котором вы могли не знать до момента компиляции (то есть перед привязкой к python шаблон тип может быть определен пользователем вашего кода). В С++ всякий раз, когда вы пишете свою основную функцию, и в этой основной вы можете указать тип шаблона во время создания объекта. В приведенном выше примере в main я предоставил тип шаблона как EmployeeParser моему классу GenericParser. И после привязки этого класса пользователь может захотеть инициировать этот EmployeeParser, возможно, с другим классом C++, скажем, MilitaryParser

Давайте начнем привязки с простого примера.

Я создал класс Interface. Это шаблонный класс. Тогда вы получите два класса impl_a.h и impl_b.h

И у нас может быть C++ main, как показано ниже.

Когда вы выполните выше main, вы увидите, что P5ImplA и P5ImplB распечатаны, как и ожидалось.

Теперь мы хотим экспортировать этот пример в python. Это означает, что вы хотите экспортировать интерфейс в python и предоставить функцию python, чтобы иметь возможность «подключить» одну из реализаций к интерфейсу во время «выполнения» (скажем, в интерпретаторе python).

Прежде чем сделать это, давайте посмотрим, как экспортировать ImplA в python статическим способом, чтобы вы могли понять, как я обобщил этот пример. Поскольку ImplA наследуется от Interface<ImplA>, вы должны экспортировать оба в Python, чтобы иметь возможность использовать ImplA. Вы можете написать что-то вроде файла bindings.cpp.

Теперь вы можете использовать ImplA в Python! Учитывая файл make.common, makefile и каталог pybind11/, вам просто нужно запустить make bindigns, чтобы запустить интерпретатор Python, который загружает модуль bindings и вызывает ImplA().print(), который будет отображать P5ImplA.

Теперь давайте посмотрим, как обобщить этот пример, чтобы мы могли динамически экспортировать реализации нашего интерфейса.

Первое, что нужно отметить, это то, что мы можем обобщить способ экспорта нашего файла Interface<ImplA>. Действительно, мы могли бы просто написать привязки для интерфейса, используя параметр шаблона для реализации. Таким образом, мы все еще можем экспортировать ImplA, используя этот bindings.cpp

И теперь идея состоит в том, чтобы написать функцию Python, которая автоматически сгенерирует эту часть файла, заменив ImplA именем реализации, которое хочет использовать пользователь.

PYBIND11_MODULE(binding, m) {
  register_interface<Interface<ImplA>>(m);
  pybind11::class_<ImplA, Interface<ImplA>>(m, "ImplA")
    .def(pybind11::init<>());
}

И, пожалуйста, обратите внимание, что ваша функция Python не должна генерировать слишком много кода. Действительно, поскольку вы записываете привязки вашего интерфейса (например, вашего API) в файл C++, используя параметр шаблона при ссылке на реализацию, вам не нужно, чтобы ваша функция python снова генерировала весь этот код для экспорта вашего API (потому что ваш реализация наследуется от интерфейса, поэтому у него уже будут доступны все API). Например, я четко экспортировал функцию print нашего интерфейса в функцию C++ register_interface(), но не повторял этого при экспорте ImplA (мне просто нужно было экспортировать его конструктор, и все).

write_pybind11_module записывает наши привязки для реализации в файл bindings.cpp в зависимости от предоставленного типа impl_t (ImplA, ImplB и т. д.). Поскольку нам нужно выбрать правильный заголовок для включения (impl_a.h, impl_b.h и т. д.) в этот файл, нам нужен небольшой файл конфигурации json, который сопоставляет предоставленный тип impl_t с путями к необходимым включениям. Затем мы должны загрузить этот конфигурационный файл, чтобы получить правильные включения, и записать это в C++ #include “impl_a.h”. Вот для чего нужен load_includes.

Наконец, нам нужна последняя функция Python для запуска write_pybind11_module, компиляции кода и предоставления новой сгенерированной общей библиотеки (наших привязок Python) к нашей текущей среде Python! Это то, что делает gen. Он запускает write_pybind11_module для записи файла bindings.cpp в зависимости от типа реализации impl_t, предоставленного пользователем (ImplA, ImplB и т. д.), затем компилирует его, вызывая make в новом процессе, и, наконец, возвращает новый модуль Python, вызывая importlib.import_module, который принимает новая сгенерированная разделяемая библиотека в параметре.

И вот, мы можем динамически экспортировать и использовать наши реализации интерфейса в Python: http://codepad.org/Ji3T3fKk (не стесняйтесь открывать новый интерпретатор Python в каталоге моего git repo и введите эти команды, это должно сработать.)

Конечно, это простой пример, но, тем не менее, это та же самая стратегия для экспорта реальной сложной библиотеки, которая использует идиому CRTP.