環境
私は、 XML DOMと非常によく似たセマンティクスと機能を備えたノードベースのツリー構造としてデータが編成される2D アニメーション システムを開発していますが、既存の XML 実装を使用するほどには似ていません。わかりやすくするために、単純化して、データ構造が次の疑似コードによって概念的に定義されていると仮定しましょう。
class Node {
Node* firstChild;
Node* nextSibling;
Node* parent;
}
最新の C++ の最先端のガイドライン ( Core Guidelines、Herb Sutter talkなど) に従うと、所有ハンドルにはスマート ポインターを使用し、非所有ハンドルには raw ポインターを使用するのが適切な選択です。所有権は一意であるため、std::unique_ptr
意味があります。
class Node {
std::unique_ptr<Node> firstChild_;
std::unique_ptr<Node> nextSibling_;
Node* parent_;
public:
Node() : parent_(nullptr) {}
static std::unique_ptr<Node> make() { return std::make_unique<Node>(); }
Node* firstChild() const { return firstChild_.get(); }
Node* nextSibling() const { return nextSibling_.get(); }
Node* parent() const { return parent_; }
Node* appendChild(std::unique_ptr<Node> child) { ... }
std::unique_ptr<Node> removeChild(Node* child) { ... }
Node* makeChild() { return appendChild(make()); }
}
この API は、クライアント コードが何をしているかを知っていることを前提としています。を使用する場合と同様にlist::iterator
、クライアントはドキュメントを読んでどの操作がノードを無効にするかを知る必要がありNode*
ます。私は C++ クライアント向けのこのアプローチに賛同しています。これは慣用的な C++ であり、パフォーマンスに対して支払う代償だと思います。
ただし、Python クライアントにより多くの安全性を提供する必要があります。アニメーション ソフトウェアは、組み込みの Python コンソールを備えた C++ GUI プログラムです。つまり、多くの Python クライアントは、プログラミング経験がほとんどなく、ほとんどがチュートリアルからコードをコピーして貼り付け、ニーズに合わせて調整するアーティストであることが期待されます。さらに悪いことに、このような信頼性の低いテストされていない Python コードは、ユーザーが潜在的に貴重な保存されていないデータを使用して長時間の対話セッションを行っている間に実行される可能性があります。Python クライアントが期限切れのノードを使用しようとしても、ソフトウェアがクラッシュすることは絶対にありません。予想される動作は、次のようなものです。
[ Embedded Python Console ]
>>> node = getSelectedNode() # allocated from C++ and selected in the GUI
>>> print(node)
<Node at 0x14e4c30 with 42 child>
>>> node.parent
<Node at 0x14e4d24 with 12 child>
>>> deleteSelection() # or via GUI interaction
>>> print(node)
<Invalid node: the node has already been deleted>
>>> print(node.parent)
InvalidNodeError: the node has already been deleted
>>>
質問
期待される動作を得るために、pybind11 を使用してこの C++ API をどのようにラップしますか?
必要に応じて、C++ API をどのように変更して、そのような動作を許可したり、ラッパー コードをより読みやすく、慣用的にしたりしますか?
すでに試したことや検討したこと
素朴な as ラッピングは機能しません。Pythonclass_<Node>(m, "Node")
インスタンスは、ノードがまだ有効かどうかを認識できません。基本的に、Node*
viaを返すと二重削除になり、 viaまたはのtake_ownership
いずれかを返すと、未定義の動作 (読み取り、セグメンテーション違反) が発生します。reference
reference_internal
PyNode
そのため、トランポリン クラスが他の手段 (たとえば、 などのコールバックの登録) を介してノードの有効性を追跡するかNode::onAboutToDie()
、参照カウント スマート ポインターを使用するように C++ 所有権モデルを変更する必要があります。
私が良い選択肢であると信じていたのは、C++ コードをshared_ptr
の代わりに使用するように変更することでしunique_ptr
た。Node
から派生し、ボンネットの下にカスタム ホルダーが格納されている場所enable_shared_from_this
としてラップします。ノードは から派生するため、所有者は生のポインタを に変換するコンストラクタを持つことができます。簡単に言えば、これは共有ポインターの参照カウント機能を利用して、所有権を共有するのではなく (所有権は依然として一意です)、Python 側で弱い参照を使用できるようにするだけです。class_<Node, PyWeakPtr<Node>>(m, "Node")
PyWeakPtr
weak_ptr
enable_shared_from_this
PyWeakPtr(T*)
weak_ptr
expired()
すべてのアクセスの前に、有効期限が切れた場合にキャッチ可能な例外をスローします。ただし、これまでのところ、私のさまざまな試みは失敗しました。期待される動作を得ることができなかったか、コンパイルすることさえできなかったなどです。
これに基づくより明白な解決策は、カスタムの holder の代わりにshared_ptr
使用することですが、Python 変数がそれらを指している限り、ノードの寿命を人為的に延長するため、どちらも機能しません。これはリークだと思います。ノードは共有されるべきではなく、ユーザーが UI でノードを削除すると、ノードは削除され、デストラクタがすぐに呼び出されます。セマンティクスは、Python で「セマンティックに削除された」ノードにアクセスすると、(C++ プログラムをクラッシュさせることなく) Python エラーが発生し、有効なノードとして黙ってマスカレードするのではありません。class_<Node, std::shared_ptr<Node>>(m, "Node")
PyWeakPtr
役立つ場合は、これらの試みのいくつかを clean/make_minimal/etc するのに時間をかけるかもしれませんが、今のところ、簡潔にするために質問をそのままにしておきます。ここにいる専門家の中には、アプローチ全体が理にかなっているなど:)
QDomDocument
Qtや Pixar のUniversal Scene Descriptionなど、同様の問題を解決する多くの既存のソフトウェアは、C++ API でも参照カウントされた弱参照 (多かれ少なかれ内部に隠されている) を使用する傾向があり、C++ と Python のクライアントの間違いから完全に保護されていることに注意してください。私はこのアプローチにもオープンですが、理想的には、C++ で所有していない生のポインターを単純に使用するという一般的なガイドラインに固執することを好むと思います。