Skip to content

Commit fd9bc8f

Browse files
oremanjwjakob
authored andcommitted
Add basic support for tag-based static polymorphism (pybind#1326)
* Add basic support for tag-based static polymorphism Sometimes it is possible to look at a C++ object and know what its dynamic type is, even if it doesn't use C++ polymorphism, because instances of the object and its subclasses conform to some other mechanism for being self-describing; for example, perhaps there's an enumerated "tag" or "kind" member in the base class that's always set to an indication of the correct type. This might be done for performance reasons, or to permit most-derived types to be trivially copyable. One of the most widely-known examples is in LLVM: https://llvm.org/docs/HowToSetUpLLVMStyleRTTI.html This PR permits pybind11 to be informed of such conventions via a new specializable detail::polymorphic_type_hook<> template, which generalizes the previous logic for determining the runtime type of an object based on C++ RTTI. Implementors provide a way to map from a base class object to a const std::type_info* for the dynamic type; pybind11 then uses this to ensure that casting a Base* to Python creates a Python object that knows it's wrapping the appropriate sort of Derived. There are a number of restrictions with this tag-based static polymorphism support compared to pybind11's existing support for built-in C++ polymorphism: - there is no support for this-pointer adjustment, so only single inheritance is permitted - there is no way to make C++ code call new Python-provided subclasses - when binding C++ classes that redefine a method in a subclass, the .def() must be repeated in the binding for Python to know about the update But these are not much of an issue in practice in many cases, the impact on the complexity of pybind11's innards is minimal and localized, and the support for automatic downcasting improves usability a great deal.
1 parent 8fbb559 commit fd9bc8f

File tree

6 files changed

+297
-25
lines changed

6 files changed

+297
-25
lines changed

docs/advanced/classes.rst

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,3 +999,86 @@ described trampoline:
999999
requires a more explicit function binding in the form of
10001000
``.def("foo", static_cast<int (A::*)() const>(&Publicist::foo));``
10011001
where ``int (A::*)() const`` is the type of ``A::foo``.
1002+
1003+
Custom automatic downcasters
1004+
============================
1005+
1006+
As explained in :ref:`inheritance`, pybind11 comes with built-in
1007+
understanding of the dynamic type of polymorphic objects in C++; that
1008+
is, returning a Pet to Python produces a Python object that knows it's
1009+
wrapping a Dog, if Pet has virtual methods and pybind11 knows about
1010+
Dog and this Pet is in fact a Dog. Sometimes, you might want to
1011+
provide this automatic downcasting behavior when creating bindings for
1012+
a class hierarchy that does not use standard C++ polymorphism, such as
1013+
LLVM [#f4]_. As long as there's some way to determine at runtime
1014+
whether a downcast is safe, you can proceed by specializing the
1015+
``pybind11::polymorphic_type_hook`` template:
1016+
1017+
.. code-block:: cpp
1018+
1019+
enum class PetKind { Cat, Dog, Zebra };
1020+
struct Pet { // Not polymorphic: has no virtual methods
1021+
const PetKind kind;
1022+
int age = 0;
1023+
protected:
1024+
Pet(PetKind _kind) : kind(_kind) {}
1025+
};
1026+
struct Dog : Pet {
1027+
Dog() : Pet(PetKind::Dog) {}
1028+
std::string sound = "woof!";
1029+
std::string bark() const { return sound; }
1030+
};
1031+
1032+
namespace pybind11 {
1033+
template<> struct polymorphic_type_hook<Pet> {
1034+
static const void *get(const Pet *src, const std::type_info*& type) {
1035+
// note that src may be nullptr
1036+
if (src && src->kind == PetKind::Dog) {
1037+
type = &typeid(Dog);
1038+
return static_cast<const Dog*>(src);
1039+
}
1040+
return src;
1041+
}
1042+
};
1043+
} // namespace pybind11
1044+
1045+
When pybind11 wants to convert a C++ pointer of type ``Base*`` to a
1046+
Python object, it calls ``polymorphic_type_hook<Base>::get()`` to
1047+
determine if a downcast is possible. The ``get()`` function should use
1048+
whatever runtime information is available to determine if its ``src``
1049+
parameter is in fact an instance of some class ``Derived`` that
1050+
inherits from ``Base``. If it finds such a ``Derived``, it sets ``type
1051+
= &typeid(Derived)`` and returns a pointer to the ``Derived`` object
1052+
that contains ``src``. Otherwise, it just returns ``src``, leaving
1053+
``type`` at its default value of nullptr. If you set ``type`` to a
1054+
type that pybind11 doesn't know about, no downcasting will occur, and
1055+
the original ``src`` pointer will be used with its static type
1056+
``Base*``.
1057+
1058+
It is critical that the returned pointer and ``type`` argument of
1059+
``get()`` agree with each other: if ``type`` is set to something
1060+
non-null, the returned pointer must point to the start of an object
1061+
whose type is ``type``. If the hierarchy being exposed uses only
1062+
single inheritance, a simple ``return src;`` will achieve this just
1063+
fine, but in the general case, you must cast ``src`` to the
1064+
appropriate derived-class pointer (e.g. using
1065+
``static_cast<Derived>(src)``) before allowing it to be returned as a
1066+
``void*``.
1067+
1068+
.. [#f4] https://llvm.org/docs/HowToSetUpLLVMStyleRTTI.html
1069+
1070+
.. note::
1071+
1072+
pybind11's standard support for downcasting objects whose types
1073+
have virtual methods is implemented using
1074+
``polymorphic_type_hook`` too, using the standard C++ ability to
1075+
determine the most-derived type of a polymorphic object using
1076+
``typeid()`` and to cast a base pointer to that most-derived type
1077+
(even if you don't know what it is) using ``dynamic_cast<void*>``.
1078+
1079+
.. seealso::
1080+
1081+
The file :file:`tests/test_tagbased_polymorphic.cpp` contains a
1082+
more complete example, including a demonstration of how to provide
1083+
automatic downcasting for an entire class hierarchy without
1084+
writing one get() function for each class.

docs/classes.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,8 @@ just brings them on par.
228228

229229
.. _inheritance:
230230

231-
Inheritance and automatic upcasting
232-
===================================
231+
Inheritance and automatic downcasting
232+
=====================================
233233

234234
Suppose now that the example consists of two data structures with an
235235
inheritance relationship:
@@ -298,7 +298,7 @@ inheritance relationship. This is reflected in Python:
298298
299299
>>> p = example.pet_store()
300300
>>> type(p) # `Dog` instance behind `Pet` pointer
301-
Pet # no pointer upcasting for regular non-polymorphic types
301+
Pet # no pointer downcasting for regular non-polymorphic types
302302
>>> p.bark()
303303
AttributeError: 'Pet' object has no attribute 'bark'
304304
@@ -330,11 +330,11 @@ will automatically recognize this:
330330
331331
>>> p = example.pet_store2()
332332
>>> type(p)
333-
PolymorphicDog # automatically upcast
333+
PolymorphicDog # automatically downcast
334334
>>> p.bark()
335335
u'woof!'
336336
337-
Given a pointer to a polymorphic base, pybind11 performs automatic upcasting
337+
Given a pointer to a polymorphic base, pybind11 performs automatic downcasting
338338
to the actual derived type. Note that this goes beyond the usual situation in
339339
C++: we don't just get access to the virtual functions of the base, we get the
340340
concrete derived type including functions and attributes that the base type may

include/pybind11/cast.h

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -774,9 +774,45 @@ template <typename T1, typename T2> struct is_copy_constructible<std::pair<T1, T
774774
: all_of<is_copy_constructible<T1>, is_copy_constructible<T2>> {};
775775
#endif
776776

777+
NAMESPACE_END(detail)
778+
779+
// polymorphic_type_hook<itype>::get(src, tinfo) determines whether the object pointed
780+
// to by `src` actually is an instance of some class derived from `itype`.
781+
// If so, it sets `tinfo` to point to the std::type_info representing that derived
782+
// type, and returns a pointer to the start of the most-derived object of that type
783+
// (in which `src` is a subobject; this will be the same address as `src` in most
784+
// single inheritance cases). If not, or if `src` is nullptr, it simply returns `src`
785+
// and leaves `tinfo` at its default value of nullptr.
786+
//
787+
// The default polymorphic_type_hook just returns src. A specialization for polymorphic
788+
// types determines the runtime type of the passed object and adjusts the this-pointer
789+
// appropriately via dynamic_cast<void*>. This is what enables a C++ Animal* to appear
790+
// to Python as a Dog (if Dog inherits from Animal, Animal is polymorphic, Dog is
791+
// registered with pybind11, and this Animal is in fact a Dog).
792+
//
793+
// You may specialize polymorphic_type_hook yourself for types that want to appear
794+
// polymorphic to Python but do not use C++ RTTI. (This is a not uncommon pattern
795+
// in performance-sensitive applications, used most notably in LLVM.)
796+
template <typename itype, typename SFINAE = void>
797+
struct polymorphic_type_hook
798+
{
799+
static const void *get(const itype *src, const std::type_info*&) { return src; }
800+
};
801+
template <typename itype>
802+
struct polymorphic_type_hook<itype, detail::enable_if_t<std::is_polymorphic<itype>::value>>
803+
{
804+
static const void *get(const itype *src, const std::type_info*& type) {
805+
type = src ? &typeid(*src) : nullptr;
806+
return dynamic_cast<const void*>(src);
807+
}
808+
};
809+
810+
NAMESPACE_BEGIN(detail)
811+
777812
/// Generic type caster for objects stored on the heap
778813
template <typename type> class type_caster_base : public type_caster_generic {
779814
using itype = intrinsic_t<type>;
815+
780816
public:
781817
static constexpr auto name = _<type>();
782818

@@ -793,32 +829,28 @@ template <typename type> class type_caster_base : public type_caster_generic {
793829
return cast(&src, return_value_policy::move, parent);
794830
}
795831

796-
// Returns a (pointer, type_info) pair taking care of necessary RTTI type lookup for a
797-
// polymorphic type. If the instance isn't derived, returns the non-RTTI base version.
798-
template <typename T = itype, enable_if_t<std::is_polymorphic<T>::value, int> = 0>
832+
// Returns a (pointer, type_info) pair taking care of necessary type lookup for a
833+
// polymorphic type (using RTTI by default, but can be overridden by specializing
834+
// polymorphic_type_hook). If the instance isn't derived, returns the base version.
799835
static std::pair<const void *, const type_info *> src_and_type(const itype *src) {
800-
const void *vsrc = src;
801836
auto &cast_type = typeid(itype);
802837
const std::type_info *instance_type = nullptr;
803-
if (vsrc) {
804-
instance_type = &typeid(*src);
805-
if (!same_type(cast_type, *instance_type)) {
806-
// This is a base pointer to a derived type; if it is a pybind11-registered type, we
807-
// can get the correct derived pointer (which may be != base pointer) by a
808-
// dynamic_cast to most derived type:
809-
if (auto *tpi = get_type_info(*instance_type))
810-
return {dynamic_cast<const void *>(src), const_cast<const type_info *>(tpi)};
811-
}
838+
const void *vsrc = polymorphic_type_hook<itype>::get(src, instance_type);
839+
if (instance_type && !same_type(cast_type, *instance_type)) {
840+
// This is a base pointer to a derived type. If the derived type is registered
841+
// with pybind11, we want to make the full derived object available.
842+
// In the typical case where itype is polymorphic, we get the correct
843+
// derived pointer (which may be != base pointer) by a dynamic_cast to
844+
// most derived type. If itype is not polymorphic, we won't get here
845+
// except via a user-provided specialization of polymorphic_type_hook,
846+
// and the user has promised that no this-pointer adjustment is
847+
// required in that case, so it's OK to use static_cast.
848+
if (const auto *tpi = get_type_info(*instance_type))
849+
return {vsrc, tpi};
812850
}
813851
// Otherwise we have either a nullptr, an `itype` pointer, or an unknown derived pointer, so
814852
// don't do a cast
815-
return type_caster_generic::src_and_type(vsrc, cast_type, instance_type);
816-
}
817-
818-
// Non-polymorphic type, so no dynamic casting; just call the generic version directly
819-
template <typename T = itype, enable_if_t<!std::is_polymorphic<T>::value, int> = 0>
820-
static std::pair<const void *, const type_info *> src_and_type(const itype *src) {
821-
return type_caster_generic::src_and_type(src, typeid(itype));
853+
return type_caster_generic::src_and_type(src, cast_type, instance_type);
822854
}
823855

824856
static handle cast(const itype *src, return_value_policy policy, handle parent) {

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ set(PYBIND11_TEST_FILES
5757
test_smart_ptr.cpp
5858
test_stl.cpp
5959
test_stl_binders.cpp
60+
test_tagbased_polymorphic.cpp
6061
test_virtual_functions.cpp
6162
)
6263

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
tests/test_tagbased_polymorphic.cpp -- test of polymorphic_type_hook
3+
4+
Copyright (c) 2018 Hudson River Trading LLC <[email protected]>
5+
6+
All rights reserved. Use of this source code is governed by a
7+
BSD-style license that can be found in the LICENSE file.
8+
*/
9+
10+
#include "pybind11_tests.h"
11+
#include <pybind11/stl.h>
12+
13+
struct Animal
14+
{
15+
enum class Kind {
16+
Unknown = 0,
17+
Dog = 100, Labrador, Chihuahua, LastDog = 199,
18+
Cat = 200, Panther, LastCat = 299
19+
};
20+
static const std::type_info* type_of_kind(Kind kind);
21+
static std::string name_of_kind(Kind kind);
22+
23+
const Kind kind;
24+
const std::string name;
25+
26+
protected:
27+
Animal(const std::string& _name, Kind _kind)
28+
: kind(_kind), name(_name)
29+
{}
30+
};
31+
32+
struct Dog : Animal
33+
{
34+
Dog(const std::string& _name, Kind _kind = Kind::Dog) : Animal(_name, _kind) {}
35+
std::string bark() const { return name_of_kind(kind) + " " + name + " goes " + sound; }
36+
std::string sound = "WOOF!";
37+
};
38+
39+
struct Labrador : Dog
40+
{
41+
Labrador(const std::string& _name, int _excitement = 9001)
42+
: Dog(_name, Kind::Labrador), excitement(_excitement) {}
43+
int excitement;
44+
};
45+
46+
struct Chihuahua : Dog
47+
{
48+
Chihuahua(const std::string& _name) : Dog(_name, Kind::Chihuahua) { sound = "iyiyiyiyiyi"; }
49+
std::string bark() const { return Dog::bark() + " and runs in circles"; }
50+
};
51+
52+
struct Cat : Animal
53+
{
54+
Cat(const std::string& _name, Kind _kind = Kind::Cat) : Animal(_name, _kind) {}
55+
std::string purr() const { return "mrowr"; }
56+
};
57+
58+
struct Panther : Cat
59+
{
60+
Panther(const std::string& _name) : Cat(_name, Kind::Panther) {}
61+
std::string purr() const { return "mrrrRRRRRR"; }
62+
};
63+
64+
std::vector<std::unique_ptr<Animal>> create_zoo()
65+
{
66+
std::vector<std::unique_ptr<Animal>> ret;
67+
ret.emplace_back(new Labrador("Fido", 15000));
68+
69+
// simulate some new type of Dog that the Python bindings
70+
// haven't been updated for; it should still be considered
71+
// a Dog, not just an Animal.
72+
ret.emplace_back(new Dog("Ginger", Dog::Kind(150)));
73+
74+
ret.emplace_back(new Chihuahua("Hertzl"));
75+
ret.emplace_back(new Cat("Tiger", Cat::Kind::Cat));
76+
ret.emplace_back(new Panther("Leo"));
77+
return ret;
78+
}
79+
80+
const std::type_info* Animal::type_of_kind(Kind kind)
81+
{
82+
switch (kind) {
83+
case Kind::Unknown: break;
84+
85+
case Kind::Dog: break;
86+
case Kind::Labrador: return &typeid(Labrador);
87+
case Kind::Chihuahua: return &typeid(Chihuahua);
88+
case Kind::LastDog: break;
89+
90+
case Kind::Cat: break;
91+
case Kind::Panther: return &typeid(Panther);
92+
case Kind::LastCat: break;
93+
}
94+
95+
if (kind >= Kind::Dog && kind <= Kind::LastDog) return &typeid(Dog);
96+
if (kind >= Kind::Cat && kind <= Kind::LastCat) return &typeid(Cat);
97+
return nullptr;
98+
}
99+
100+
std::string Animal::name_of_kind(Kind kind)
101+
{
102+
std::string raw_name = type_of_kind(kind)->name();
103+
py::detail::clean_type_id(raw_name);
104+
return raw_name;
105+
}
106+
107+
namespace pybind11 {
108+
template <typename itype>
109+
struct polymorphic_type_hook<itype, detail::enable_if_t<std::is_base_of<Animal, itype>::value>>
110+
{
111+
static const void *get(const itype *src, const std::type_info*& type)
112+
{ type = src ? Animal::type_of_kind(src->kind) : nullptr; return src; }
113+
};
114+
}
115+
116+
TEST_SUBMODULE(tagbased_polymorphic, m) {
117+
py::class_<Animal>(m, "Animal")
118+
.def_readonly("name", &Animal::name);
119+
py::class_<Dog, Animal>(m, "Dog")
120+
.def(py::init<std::string>())
121+
.def_readwrite("sound", &Dog::sound)
122+
.def("bark", &Dog::bark);
123+
py::class_<Labrador, Dog>(m, "Labrador")
124+
.def(py::init<std::string, int>(), "name"_a, "excitement"_a = 9001)
125+
.def_readwrite("excitement", &Labrador::excitement);
126+
py::class_<Chihuahua, Dog>(m, "Chihuahua")
127+
.def(py::init<std::string>())
128+
.def("bark", &Chihuahua::bark);
129+
py::class_<Cat, Animal>(m, "Cat")
130+
.def(py::init<std::string>())
131+
.def("purr", &Cat::purr);
132+
py::class_<Panther, Cat>(m, "Panther")
133+
.def(py::init<std::string>())
134+
.def("purr", &Panther::purr);
135+
m.def("create_zoo", &create_zoo);
136+
};

tests/test_tagbased_polymorphic.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pybind11_tests import tagbased_polymorphic as m
2+
3+
4+
def test_downcast():
5+
zoo = m.create_zoo()
6+
assert [type(animal) for animal in zoo] == [
7+
m.Labrador, m.Dog, m.Chihuahua, m.Cat, m.Panther
8+
]
9+
assert [animal.name for animal in zoo] == [
10+
"Fido", "Ginger", "Hertzl", "Tiger", "Leo"
11+
]
12+
zoo[1].sound = "woooooo"
13+
assert [dog.bark() for dog in zoo[:3]] == [
14+
"Labrador Fido goes WOOF!",
15+
"Dog Ginger goes woooooo",
16+
"Chihuahua Hertzl goes iyiyiyiyiyi and runs in circles"
17+
]
18+
assert [cat.purr() for cat in zoo[3:]] == ["mrowr", "mrrrRRRRRR"]
19+
zoo[0].excitement -= 1000
20+
assert zoo[0].excitement == 14000

0 commit comments

Comments
 (0)