pybind11中虚函数与继承结合

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类必须重写所有虚函数,包括:

  • 从Animal继承的虚函数(即使Dog已经提供了实现)

  • Dog自己添加的新虚函数

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; // 继承构造函数

// 重写从Animal继承的虚函数
std::string name() override {
PYBIND11_OVERRIDE(
std::string, // 返回类型
Dog, // 父类(现在是Dog,不是Animal)
name // 函数名,无参数时的写法
);
}

std::string go(int n_times) override {
PYBIND11_OVERRIDE(
std::string, // 返回类型
Dog, // 父类
go, // 函数名
n_times // 参数
);
}

// 重写Dog添加的新虚函数
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类中,第二个参数应该是直接父类,而不是最顶层的基类:

  • 在PyDog中使用Dog而不是Animal

  • 在PyHusky中使用Husky而不是DogAnimal

4.3 纯虚函数vs普通虚函数

  • 使用PYBIND11_OVERRIDE_PURE处理纯虚函数

  • 使用PYBIND11_OVERRIDE处理有默认实现的虚函数

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; // 继承构造函数

// 重写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
);
}

// 添加Dog特有的虚函数
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) {
// Animal使用基础模板
py::class_<Animal, PyAnimalT<>,py::smart_holder> animal(m, "Animal");
animal.def(py::init<>())
.def("name", &Animal::name)
.def("go", &Animal::go);

// Dog使用Dog模板
py::class_<Dog, PyDogT<>,py::smart_holder> dog(m, "Dog");
dog.def(py::init<>())
.def("bark", &Dog::bark);

// Husky不需要专门的模板,使用Dog模板即可
py::class_<Husky, PyDogT<Husky>,py::smart_holder> husky(m, "Husky");
husky.def(py::init<>());
}

5.3 模板技术的优势

  1. 减少代码重复:只需实现一次虚函数重写

  2. 灵活性:可以轻松适应类层次结构的变化

  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()) # 输出: Dog
print(dog.bark()) # 输出: woof!
print(dog.go(2)) # 输出: woof! woof!

# 在Python中继承Animal
class Cat(Animal):
def name(self):
return "Cat"

def go(self, n_times):
return "meow! " * n_times

cat = Cat()
print(cat.name()) # 输出: Cat
print(cat.go(3)) # 输出: meow! meow! meow!

# 在Python中继承Dog
class Puppy(Dog):
def name(self):
return "Puppy"

def bark(self):
return "yap!"

puppy = Puppy()
print(puppy.name()) # 输出: Puppy
print(puppy.bark()) # 输出: yap!
print(puppy.go(2)) # 输出: yap! yap!

# 在Python中继承Husky
class HuskyPuppy(Husky):
def bark(self):
return "aroooo!"

husky_pup = HuskyPuppy()
print(husky_pup.name()) # 输出: Dog (继承自Dog)
print(husky_pup.bark()) # 输出: aroooo!
print(husky_pup.go(2)) # 输出: aroooo! aroooo!

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()) # 输出: Super Dog
print(super_dog.bark()) # 输出: WOOF!
print(super_dog.go(2)) # 输出: WOOF! WOOF!
print(super_dog.fly()) # 输出: I'm flying!

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;

// C++类定义
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 {};

// 模板trampoline类
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 绑定要点总结

  1. 模板参数顺序:trampoline类作为第二个模板参数

  2. 绑定目标:始终绑定到原始C++类,不是trampoline类

  3. 继承关系:正确指定C++类之间的继承关系

  4. 函数签名:确保Python和C++的函数签名一致

8. 性能考虑

8.1 Trampoline初始化时机

  • Trampoline类只在Python实际继承时初始化

  • 单纯创建实例不会初始化trampoline

  • 这是出于性能优化的考虑

8.2 模板vs非模板性能

  • 模板trampoline可能生成更多的代码

  • 但现代编译器优化通常能消除额外开销

  • 代码维护性的提升通常超过微小的性能差异

9. 常见问题和解决方案

9.1 函数名不匹配

问题:Python中重写的函数不被调用
原因:C++和Python函数名不匹配
解决方案:使用PYBIND11_OVERRIDE_NAME

1
2
3
4
5
6
7
8
9
// 当C++函数名与Python函数名不同时
std::string operator()() override {
PYBIND11_OVERRIDE_NAME(
std::string,
MyClass,
"__call__", // Python中的函数名
operator() // C++中的函数名
);
}

9.2 继承切片问题

问题:通过基类指针访问时丢失派生类信息
解决方案:使用py::smart_holder代替std::shared_ptr

9.3 内存泄漏

问题:使用自定义trampoline导致内存泄漏
解决方案:确保正确继承py::trampoline_self_life_support