I have a C++ class RuleSet
with a bunch of std::shared_ptr
to some rules for a game, most notably to the victory condition, a std::shared_ptr<VictoryRule>
. VictoryRule
is a abstract C++ type, with one function winners
to return the winners of a game. (team_t
is a enum, but for simplicity consider it an alias to int
.)
class VictoryRule {
public:
//! @brief Check if game is won by a team.
//! @return the teamno of the team that won or team_t::no_team if nobody won.
virtual std::vector<team_t> winners(const Game &game) = 0;
//! @brief Default virtual destructor.
virtual ~VictoryRule() = default;
};
I exposed this class to python with pybind11 to be able to subclass it in python, with a trampoline class.
class PyVictoryRule : public VictoryRule {
public:
std::vector<team_t> winners(const Game &game) override {
PYBIND11_OVERRIDE_PURE(std::vector<team_t>, VictoryRule, winners, game);
}
};
And it's registered with:
pybind11::class_<VictoryRule, PyVictoryRule, std::shared_ptr<VictoryRule>>(
m, "VictoryRule")
.def(pybind11::init())
.def("winners", &VictoryRule::winners, arg("game"));
And I'v reimplemented the Rule in python
class CustomVictoryRule(VictoryRule):
def __init__(self):
VictoryRule.__init__(self)
def winners(self, game):
return [1]
I can instantiate the class in python and call the winners
method without any issue.
cvr = CustomVictoryRule()
cvr.winners() # Returns [1]
But I don't want to use the rule directly, I want to store it in the C++ Ruleset.
class RuleSet {
public:
std::shared_ptr<VictoryRule> get_victory_rule();
void register_victory_rule(std::shared_ptr<VictoryRule> victory_rule);
private:
std::unordered_map<CasePawn::Type, std::shared_ptr<Rule>> per_pawn_type_rule_;
std::shared_ptr<VictoryRule> victory_rule_;
};
All methods of RuleSet are registered to pybind11 as-is.
pybind11::class_<RuleSet, std::shared_ptr<RuleSet>>(m, "RuleSet")
.def(pybind11::init())
.def("get_victory_rule", &RuleSet::get_victory_rule)
.def("register_victory_rule", &RuleSet::register_victory_rule,
arg("victory_rule"));
But when I pass along a CustomVictoryRule
to RuleSet.register_victory_rule
, it loses its dynamic type, and Receive an error for trying to call pure virtual function VictoryRule::winners
...
ruleset = RuleSet()
ruleset.register_victory_rule(CustomVictoryRule())
ruleset.get_victory_rule().winners() #fails with RuntimeError: `Tried to call pure virtual function "VictoryRule::winners"`
What should I do so that ruleset.get_victory_rule() return a victory rule with the correct type CustomVictoryRule, so that CustomVictoryRule.winners() is called in that last line of code ?
I found the issue. Instead of doing:
ruleset.register_victory_rule(CustomVictoryRule())
I do:
cvr = CustomVictoryRule()
ruleset.register_victory_rule(cvr)
Then it worked ! So somehow python keeps alive the instance when it's stored into a local variable that's kept alive as long as the ruleset
.
Thus, I need to tell pybind11 to keep temporary python objects passed as arguments. Luckily, there's a pybind11::keep_alive
decorator to functions to do exactly that. So, binding RuleSet
from c++ to python with the following fixes the issue !
pybind11::class_<RuleSet, std::shared_ptr<RuleSet>>(m, "RuleSet")
.def(pybind11::init())
.def("victory_rule", &RuleSet::get_victory_rule)
.def("register_victory_rule", &RuleSet::register_victory_rule,
arg("victory_rule"), keep_alive<1, 2>());