pybind11虚函数与继承结合
1. 概述
当在pybind11中处理具有继承关系的C++类层次结构时,重写虚函数需要特别注意。特别是当一个类继承自另一个已经有trampoline类的类时,需要正确处理所有继承的虚函数。
2. 多层继承的挑战
2.1 扩展的类层次结构
考虑以下扩展的C++类层次结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Animal { public: virtual ~Animal() {} virtual std::string name() = 0; virtual std::string go(int n_times) = 0; };
class Dog : public Animal { public: std::string name() override { return "Dog"; } std::string go(int n_times) override { std::string result; for (int i = 0; i < n_times; ++i) result += bark() + " "; return result; } virtual std::string bark() { return "woof!"; } };
class Husky : public Dog {};
|
2.2 简单trampoline的局限性
如果只为Animal创建trampoline类,Python代码无法正确继承Dog类。因为:
-
Dog添加了新的虚函数bark()
-
Dog继承并重写了Animal的虚函数
-
Python需要能够重写所有这些虚函数
3. 正确的trampoline实现
3.1 Animal的trampoline类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class PyAnimal : public Animal, public py::trampoline_self_life_support { public: using Animal::Animal; std::string name() override { PYBIND11_OVERRIDE_PURE( std::string, Animal, name ); } std::string go(int n_times) override { PYBIND11_OVERRIDE_PURE( std::string, Animal, go, n_times ); } };
|
3.2 Dog的trampoline类
重要:Dog的trampoline类必须重写所有虚函数,包括:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class PyDog : public Dog, public py::trampoline_self_life_support { public: using Dog::Dog; std::string name() override { PYBIND11_OVERRIDE( std::string, Dog, name ); } std::string go(int n_times) override { PYBIND11_OVERRIDE( std::string, Dog, go, n_times ); } std::string bark() override { PYBIND11_OVERRIDE( std::string, Dog, bark ); } };
|
3.3 无新虚函数的派生类
即使Husky没有添加新的虚函数,也需要trampoline类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class PyHusky : public Husky, public py::trampoline_self_life_support { public: using Husky::Husky; std::string name() override { PYBIND11_OVERRIDE( std::string, Husky, name ); } std::string go(int n_times) override { PYBIND11_OVERRIDE( std::string, Husky, go, n_times ); } std::string bark() override { PYBIND11_OVERRIDE( std::string, Husky, bark ); } };
|
4. 关键注意事项
4.1 无参数函数的特殊处理
注意无参数函数在宏中的写法:
-
正确:PYBIND11_OVERRIDE(std::string, Dog, name)
-
错误:PYBIND11_OVERRIDE(std::string, Dog, name,)(有逗号)
pybind11需要这种特殊语法来正确处理无参数函数。
4.2 父类指定
在每个trampoline类中,第二个参数应该是直接父类,而不是最顶层的基类:
4.3 纯虚函数vs普通虚函数
5. 避免代码重复的模板技术
5.1 模板trampoline基类
为了避免在每个trampoline类中重复实现相同的虚函数,可以使用模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| template <class AnimalBase = Animal> class PyAnimalT : public AnimalBase, public py::trampoline_self_life_support { public: using AnimalBase::AnimalBase; std::string name() override { PYBIND11_OVERRIDE_PURE( std::string, AnimalBase, name ); } std::string go(int n_times) override { PYBIND11_OVERRIDE_PURE( std::string, AnimalBase, go, n_times ); } };
template <class DogBase = Dog> class PyDogT : public PyAnimalT<DogBase> { public: using PyAnimalT<DogBase>::PyAnimalT; std::string name() override { PYBIND11_OVERRIDE( std::string, DogBase, name ); } std::string go(int n_times) override { PYBIND11_OVERRIDE( std::string, DogBase, go, n_times ); } std::string bark() override { PYBIND11_OVERRIDE( std::string, DogBase, bark ); } };
|
5.2 使用模板trampoline
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| PYBIND11_MODULE(example, m) { py::class_<Animal, PyAnimalT<>,py::smart_holder> animal(m, "Animal"); animal.def(py::init<>()) .def("name", &Animal::name) .def("go", &Animal::go); py::class_<Dog, PyDogT<>,py::smart_holder> dog(m, "Dog"); dog.def(py::init<>()) .def("bark", &Dog::bark); py::class_<Husky, PyDogT<Husky>,py::smart_holder> husky(m, "Husky"); husky.def(py::init<>()); }
|
5.3 模板技术的优势
-
减少代码重复:只需实现一次虚函数重写
-
灵活性:可以轻松适应类层次结构的变化
-
可维护性:修改虚函数时只需在一个地方更改
6. Python中的使用示例
6.1 继承和重写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| from example import *
dog = Dog() print(dog.name()) print(dog.bark()) print(dog.go(2))
class Cat(Animal): def name(self): return "Cat" def go(self, n_times): return "meow! " * n_times
cat = Cat() print(cat.name()) print(cat.go(3))
class Puppy(Dog): def name(self): return "Puppy" def bark(self): return "yap!"
puppy = Puppy() print(puppy.name()) print(puppy.bark()) print(puppy.go(2))
class HuskyPuppy(Husky): def bark(self): return "aroooo!"
husky_pup = HuskyPuppy() print(husky_pup.name()) print(husky_pup.bark()) print(husky_pup.go(2))
|
6.2 多层继承的优势
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class SuperDog(Dog): def name(self): return "Super " + super().name() def bark(self): return super().bark().upper() def fly(self): return "I'm flying!"
super_dog = SuperDog() print(super_dog.name()) print(super_dog.bark()) print(super_dog.go(2)) print(super_dog.fly())
|
7. 绑定代码的最佳实践
7.1 完整的绑定代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| #include <pybind11/pybind11.h> #include <string>
namespace py = pybind11;
class Animal { public: virtual ~Animal() {} virtual std::string name() = 0; virtual std::string go(int n_times) = 0; };
class Dog : public Animal { public: std::string name() override { return "Dog"; } std::string go(int n_times) override { std::string result; for (int i = 0; i < n_times; ++i) result += bark() + " "; return result; } virtual std::string bark() { return "woof!"; } };
class Husky : public Dog {};
template <class AnimalBase = Animal> class PyAnimalT : public AnimalBase, public py::trampoline_self_life_support { public: using AnimalBase::AnimalBase; std::string name() override { PYBIND11_OVERRIDE_PURE( std::string, AnimalBase, name ); } std::string go(int n_times) override { PYBIND11_OVERRIDE_PURE( std::string, AnimalBase, go, n_times ); } };
template <class DogBase = Dog> class PyDogT : public PyAnimalT<DogBase> { public: using PyAnimalT<DogBase>::PyAnimalT; std::string name() override { PYBIND11_OVERRIDE( std::string, DogBase, name ); } std::string go(int n_times) override { PYBIND11_OVERRIDE( std::string, DogBase, go, n_times ); } std::string bark() override { PYBIND11_OVERRIDE( std::string, DogBase, bark ); } };
PYBIND11_MODULE(example, m) { py::class_<Animal, PyAnimalT<>,py::smart_holder> animal(m, "Animal"); animal.def(py::init<>()) .def("name", &Animal::name) .def("go", &Animal::go); py::class_<Dog, PyDogT<>,py::smart_holder> dog(m, "Dog"); dog.def(py::init<>()) .def("bark", &Dog::bark); py::class_<Husky, PyDogT<Husky>,py::smart_holder> husky(m, "Husky"); husky.def(py::init<>()); }
|
7.2 绑定要点总结
-
模板参数顺序:trampoline类作为第二个模板参数
-
绑定目标:始终绑定到原始C++类,不是trampoline类
-
继承关系:正确指定C++类之间的继承关系
-
函数签名:确保Python和C++的函数签名一致
8. 性能考虑
8.1 Trampoline初始化时机
8.2 模板vs非模板性能
-
模板trampoline可能生成更多的代码
-
但现代编译器优化通常能消除额外开销
-
代码维护性的提升通常超过微小的性能差异
9. 常见问题和解决方案
9.1 函数名不匹配
问题:Python中重写的函数不被调用
原因:C++和Python函数名不匹配
解决方案:使用PYBIND11_OVERRIDE_NAME宏
1 2 3 4 5 6 7 8 9
| std::string operator()() override { PYBIND11_OVERRIDE_NAME( std::string, MyClass, "__call__", operator() ); }
|
9.2 继承切片问题
问题:通过基类指针访问时丢失派生类信息
解决方案:使用py::smart_holder代替std::shared_ptr
9.3 内存泄漏
问题:使用自定义trampoline导致内存泄漏
解决方案:确保正确继承py::trampoline_self_life_support