先日、同僚がそのようなタスクを投げました:
「
人間と
犬の 2種類のオブジェクトがあります。 人間は
犬を飼っている (または
飼っていない)場合があります。
犬は
ホストを持っているかもしれません(持っていないかもしれません)。
Human型のオブジェクトが
Dog型のオブジェクトを所有している場合、
Dog型のこのオブジェクトについては、
Human型のこのオブジェクトがマスターであり、彼だけである
ことは
明らかです。 そして、
犬は彼の
人間が誰であるか、そしてその逆も知っているべきです。 どのように実装しますか?」
すべてがシンプルに思えます。Humanクラスと
Dogクラスで相互に2つのポインタを取得してみましょう。 しかし、このベンチャーの実装により、新しいデザインパターンのアイデアが生まれました。
テンプレートでない場合は、少なくとも静的な型制御といくつかの便利な「便利な機能」を備えたプログラムで双方向リンクを使用できるC ++イディオム。
注: 「リンク」という用語は、
「C ++リンク」の意味ではなく、「接続」の意味で記事で使用されています。
まず、ポインターを使用した最初のアイデアが悪いものを見てみましょう。
リーシュv.1(「額に」)
class Dog; class Human; class Dog { Human* link; public: Dog(): link(NULL) {} void SetLink(Human* l) { link = l; } const Human* GetLink() const { return link; } } class Human { Dog* link; public: Human(): link(NULL) {} void SetLink(Dog* l) { link = l; } const Dog* GetLink() const { return link; } }
このような実装により、「額に入れて」と言われるように、問題を解決できます。
Human *h = new Human(); Dog *d = new Dog(); h->SetLink(d); d->SetLink(h);
ただし、明らかな欠点がいくつかあります。
- バックリンクの設定を忘れることがあります:
d->SetLink(h)
。 - 他のオブジェクトへの逆方向リンクを誤って設定する可能性があります:
d->SetLink(h2)
。 - 関連オブジェクトの1つを破棄した後、他のオブジェクトは破棄されたオブジェクトを参照します。
これらの欠点を取り除くのは簡単です。
Leash v.2(自動)
class Dog; class Human; class Dog { Human* link; public: Dog(): link(NULL) {} ~Dog() { if (link) link->SetLink(NULL); } void SetLink(Human* l) { if (link == l) return; Human* oldlink = link; link = NULL; if (oldlink) oldlink->SetLink(NULL); link = l; if (link) link->SetLink(this); } const Human* GetLink() const { return link; } } class Human { Dog* link; public: Human(): link(NULL) {} ~Human() { if (link) link->SetLink(NULL); } void SetLink(Dog* l) { if (link == l) return; Dog* oldlink = link; link = NULL; if (oldlink) oldlink->SetLink(NULL); link = l; if (link) link->SetLink(this); } const Dog* GetLink() const { return link; } }
何が変わった?
まず、
SetLink()
メソッドが
SetLink()
複雑になりました。 これで、1つの方向にリンクを設定すると、メソッドは自動的に他の方向にリンクを設定します。 リンクの1つをインストールすることを忘れることはできません。 間違ったバックリンクを設定することは不可能です。
Human *h1, *h2; h1 = new Human(); h2 = new Human(); Dog *d = new Dog(); h1->SetLink(d); assert(h1->GetLink()->GetLink() == h1);
第二に、オブジェクトが破棄されると、関連するオブジェクトがあればリンクを自動的に削除するデストラクタが登場しました。 したがって、
GetLink()
常に有効なポインターまたはNULLを期待できます。
さて、今、それはすべての欠点が取り除かれ、あなたがそれを使用できるように思えます。 ただし、注意深い読者は、
Humanクラスと
Dogクラスの実装が同一であり、使用されるタイプのみが異なることに気付くでしょう。 そして、私は考えました:「テンプレートのためにこれをやり直す必要がある!」と言われています-完了しました。
Leash v.3(テンプレート)
テンプレートには、1つ以上のタイプのパラメーター化が含まれます。 この場合、これらのタイプには
M (My)と
L (Link)の2つがあり、テンプレート自体はそれぞれ
O <M、L>と記述されます。 このようなテンプレートの特殊化のインスタンスは、タイプ
Lのオブジェクトへのポインターを格納できます。つまり、接続
M-> Lを編成します
。 クラス
Aとクラス
Bの間の双方向通信を作成するために
、O <A、B>と
O <B、A>という 2つの対称的な専門化が使用されます。
したがって、タイプ
Mはリンクの
内側の端のタイプを示し、
thisポインターを正しくキャストするために必要です。タイプ
Lはリンクの
外側の端のタイプを示します。
template<class M, class L> class O { L* link; public: O(): link(NULL) {} ~O() { if (link) link->SetLink(NULL); } void SetLink(L* l) { if (link == l) return; L* oldlink = link; link = NULL; if (oldlink) oldlink->SetLink(NULL); link = l; if (link) link->SetLink(static_cast<M*>(this)); } const L* GetLink() const { return link; } };
このテンプレートクラスの使用は非常に簡単です。
class Human; class Dog; class Human: public O<Human, Dog> {}; class Dog: public O<Dog, Human> {}; Human* h = new Human(); Dog* d = new Dog(); h->SetLink(d);
いいね! クラスを2倍短く記録しました!
この双方向リンクの実装の優れた機能は、コンパイラがいくつかの
論理エラーを判断できることです。
たとえば、リンクの逆数であるクラスが存在しない。 つまり、リンク
Human- > Dogを含むことができるクラスがコードに含まれているが、リンク
Dog- > Humanを含むことができるクラスがない場合、このコードはコンパイルされません。
template<class M, class L> class O { ... }; class Human; class Dog; class Human: public O<Human, Dog> {}; class Dog {};
もちろん、同じタイプのオブジェクトをバインドすることは許可されています。
template<class M, class L> class O { ... }; class Human: public O<Human, Human> {}; Human *h1, *h2; ... h1->SetLink(h2);
自分へのリンクを設定することもできます。 これが受け入れられない場合、それに
SetLink()
て
SetLink()
メソッドを修正するのは簡単です。
今ではとても良いようです! しかし、
Dogオブジェクトへの参照に加えて、
Catオブジェクトへの参照に
人間が必要な場合はどうでしょうか?
リーシュv.4(複数のリンク)
これは、多重継承とわずかに変更された
Oクラスを使用して簡単に配置することもできます。
template<class M, class L> class O { L* link; public: O(): link(NULL) {} ~O() { if (link) link->O<L,M>::SetLink(NULL); } void SetLink(L* l) { if (link == l) return; L* oldlink = link; link = NULL; if (oldlink) oldlink->O<L,M>::SetLink(NULL); link = l; if (link) link->O<L,M>::SetLink(static_cast<M*>(this)); } const L* GetLink() const { return link; } }; class Human; class Dog; class Cat; class Human: public O<Human, Dog>, public O<Human, Cat> {}; class Dog: public O<Dog, Human> {}; class Cat: public O<Cat, Human> {};
SetLink
関数が正しい基本クラスから
SetLink
ように、
O <L、M> SetLink
の明示的な解決
SetLink
た。 熱心な読者は、オーバーロードされた関数の場合のように、渡されたパラメーターのタイプによってコンパイラーが
SetLink()
正しい
SetLink()
関数を選択していることに気付くでしょう。 ただし、これらの関数が異なるクラス(この場合は異なる
親クラス)にある場合、C ++標準に従って、オーバーロードは機能しません[1]。
同じ変更は、クラスのユーザーに影響します。
Human* h; Dog* d; Cat* c; ...
これは、
Humanクラスを少し改良することで簡素化できます。 タイプに応じて親クラスの対応するメソッドを呼び出す独自のメソッド
SetLink()
および
GetLink()
のテンプレート実装を追加します。
class Human: public O<Human, Dog>, public O<Human, Cat> { public: template<class T> const T* GetLink() const { return O<Human,T>::GetLink(); } template<class T> void SetLink(T* l) { O<Human,T>::SetLink(l); } };
これで、すべてが単一の接続とほぼ同じくらい簡単になりました。
Human* h; Dog* d; Cat* c; ... h->SetLink<Dog>(d); h->SetLink<Cat>(c);
どうしたの?
私の意見では、便利なテンプレートクラスであることが判明しました。これにより、厳密に
定義されたタイプのオブジェクト間で、
整合性を自動的に維持しながら
双方向通信を確立できます。
はい、テンプレートクラスを文字
Oと呼んだ理由を聞かないでください
。 何よりも、
リーシュ (リーシュ)という名前が彼に合っているでしょう。
書かれたものはgcc 4.4.3を使用してテストされました。
参照資料
[1]
bytes.com/topic/c/answers/137040-multiple-inheritance-ambiguityPS。 誰かが現在のC ++標準のオンラインバージョンを知っている場合は、リンクを共有してください。元のソースを参照したいと思います。
PPS このトピックはSandboxでの遅い火事に苦しんでいましたが、複数のリンク、さらにC ++ 0xを使用する場合に
Oの使用を簡素化する方法のアイデアを得ました。 お楽しみに。