Referenced型オブジェクト

概要

Choreonoidでは動的メモリ確保によって生成されるオブジェクトの多くが、Referencedクラスを基底とするクラスとして定義されています。 そしてそのようなクラスはChoreonoid独自のスマートポインタであるref_ptr型のポインタで保持されます。 この形態はChoreonoid SDKの至るところで利用されていて、独自のプラグインを作る際に利用する機会も多いかと思います。 そこで本節ではそのようなReferenced型オブジェクトの生成と保持について、基本的な事柄を解説します。

オブジェクトの動的生成とスマートポインタ

生存期間が比較的長く、プログラムの複数の個所から共有されるようなオブジェクトは、通常new演算子によって生成し、動的に確保されたメモリ上に配置され、ポインタでアクセスすることになります。この場合、オブジェクトが必要でなくなったタイミングでdelete演算子によって削除し、オブジェクトの後始末やメモリの解放を行わなければなりません。ただしこの処理を漏れなくコーディングすることはプログラマにとっては負担であり、実際に削除されないままメモリリーク等の問題を起こしてしまうことも多々あります。

そこで動的に生成されポインタで参照されるオブジェクトに関しては、所謂スマートポインタで保持することが一般的となっています。スマートポインタで保持しておけば、ポインタ変数がスコープから外れ、オブジェクトがどこからも参照されなくなったタイミングで、自動的にオブジェクトの削除が行われます。C++ではこのためのスマートポインタとしてstd::unique_ptrやstd::shared_ptrを標準ライブラリに備えており、それらを使うことで動的に生成するオブジェクトの破棄をより確実にすることができます。

Choreonoidの実装でも標準ライブラリのスマートポインタは使用しているのですが、その一方で独自のオブジェクト型とそれに特化したスマートポインタも使用しています。従ってChoreonoidのプラグイン開発では、両者の違いについて把握し、両者を使い分けることが必要となってきます。

Referenced型のオブジェクトとref_ptr型のスマートポインタ

Choreonoidの実装における「独自のオブジェクト型とそれに特化したスマートポインタ」は、それぞれReferenced型とref_ptr型が対応します。これらはオブジェクト内蔵式の参照カウンタによって動的オブジェクトの共有を行う仕組みとなっています。

動的に生成するオブジェクトの保持にこれを利用する場合は、オブジェクトのクラスをReferenced型を継承して定義します。例えばObjectというクラスにこれを適用するとしたら、以下のようにします。

#include <cnoid/Referenced>

class Object : public cnoid::Referenced
{
public:
    void function();

    ...

};

これでObjectクラスはReferenced型になります。このクラスは通常newで生成します。そして生成したポインタの保持にref_ptr型のスマートポインタを使用します。これは以下のように記述できます。

cnoid::ref_ptr<Object> object = new Object;

objectは通常のポインタ(生ポインタ)と同様に扱えます。例えば参照先オブジェクトのメンバ関数は以下のようにして実行できます。

object->fundtion();

あとは毎回ref_ptrで記述すると若干記述が複雑になるので、typedefで予めこのクラス専用のポインタ型として定義しておくことが多いです。このケースでは

typedef ref_ptr<Object> ObjectPtr;

としてtypedefしておき、

ObjectPtr object = new Object;

などと記述します。このようにtypedefする型名としては「クラス名 + Ptr」という名前にします。このルールとすることで、typedefした型名についてもスマートポインタ型であると認識できます。

スマートポインタなので、このポインタが破棄されるタイミングで参照しているオブジェクトも破棄されます。従ってオブジェクトに対して明示的にdeleteを実行する必要はありませんし、してはいけません。例えば

{
    ObjectPtr object = new Object;

    ...

}

となっていると、このスコープの終了時点でobjectもdeleteされます。

もちろん実際の利用例はこのような単純なスコープであることはあまりありません。実際にはこのポインタが他のオブジェクトのメンバ変数として定義されていて、そこから参照されていたり、さらにそれが複数のオブジェクトに渡っていたりします。ref_ptrによる参照はそのように複数のポインタが同一のオブジェクトを重複して参照することが可能です。その場合、どのポインタからも参照されなくなった時点で、オブジェクトが破棄されます。

ここまでみてもらえば分かるように、あるオブジェクトを複数のポインタで共有して参照できるスマートポインタということで、これは基本的にはstd::shared_ptrと同様のものです。これを実現するための方法も基本的には同じで、どちらも参照カウンタを用いています。

shared_ptrとref_ptrの違い

Referenced型で定義したクラスをref_ptrで参照する目的は、基本的にstd::shared_ptrを利用する場合と同じです。 では両者の違いはどこにあるかと言えば、ref_ptr型の方がshared_ptrに比べてより生ポインタとの親和性が高いところにあります。 このことにより、両者でコーディングのスタイルが若干異なってきます。

まずオブジェクトの生成時に、shared_ptrの場合は

shared_ptr<Object> object = new Object;

とするよりも

shared_ptr<Object> object = make_shared<Object>();

とした方が若干効率が高くなるため、基本的にはこちらの記述をします。Referenced + ref_ptrの場合は常に前者の記述になります。

注釈

これについては、動的に生成することが前提の場合は、例えば生成用のcreateというstaticメンバ関数を定義して、

shared_ptr<Object> object = Object::create();

とし、生成方法を限定する流儀もあります。この場合はshared_ptrとref_ptrで特に差は生じません。

また、shared_ptrの場合は、これを利用する以上全ての場面でshared_ptrを使用する前提となります。例えば

shared_ptr<Object> object = make_shared<Object>();
...

Object* object2 = object.get();
...

shared_ptr<Object> object3 = object2;

といったことはできません。生ポインタであるobject2からshared_ptrを再生成すると、これは元のshared_ptrと参照の管理が競合してしまうからです。

一方でReferenced + ref_ptrの場合はこのように途中で生ポインタを経由することも問題ありません。オブジェクトへの参照を継続的に保持するポインタのみをref_ptrとし、それ以外に関数の引数などで一時的に参照される場合は、生ポインタにしてしまってもOKです。実際にChoreonoidの実装ではそのようにref_ptrと生ポインタを使い分けているところが多くあります。

注釈

これについても、enable_shared_from_thisを導入することにより、shared_ptrでも同様の使い分けができると思われます。通常はそのようなことを積極的にすることはないと思われますが…。

以上まとめると、Refernced + ref_ptrは生ポインタと組み合わせて記述しやすいところがあり、これがshared_ptrと一番異なる点です。この特性によりコードの記述を若干簡潔にできることがあります。またその場合参照カウンタに関わるオーバーヘッドも少なくなるので、使用状況によってはパフォーマンスが微妙に良くなる可能性もあります。

このような差異は、参照カウンタをオブジェクトの外部に確保するかオブジェクト自身に持たせるかの違いからきています。shared_ptrは前者の方法ですが、Referenced + ref_ptrの場合はReferencedクラスのメンバ変数として定義されている参照カウンタが使用されます。

Referenced + ref_ptrによるスマートポインタは、Choreonoid開発の初期段階において試行錯誤の上で導入され、これまで利用が継続してきました。C++標準のスマートポインタであるshared_ptrとは若干使用方法が異なるので、その点には注意してください。

weak_ref_ptr

shared_ptrでは弱参照を保持するweak_ptrも利用可能ですが、ref_ptrについても弱参照を保持するためのweak_ref_ptrを使用可能です。利用方法はweak_ptrとほぼ同じです。以下のようにref_ptrから生成できます。

ref_ptr<Object> object = new Object;
weak_ref_ptr<Object> wobject = object;

ref_ptrと同様に生ポインタからも生成できます。

weak_ref_ptr<Object> wobject = new Object;

weak_ptrと同様にlock関数でref_ptrを生成できます。

if(ref_ptr<Object> obj = wobject.lock()){
    ...
}

他にreset、expiredといった関数もweak_ptrと同様に使用できます。

独自クラスのReferenced化について

プラグインの実装において独自のクラスを定義する場合も、Referenced型を継承してref_ptrで参照することが可能です。 Referenced型である既存のクラスを継承する場合は、新たに定義するクラスも必然的にReferenced型となりますが、そうでない場合は必ずしもReferenced型とする必要はありません。 newを用いて動的に生成することが前提のクラスで、スマートポインタによる参照を行いたい場合は、Referenced型とする候補となります。 そのように使用する可能性があっても、自動変数やクラスのメンバ変数として直接定義して使用する可能性もある場合は、Referenced型にしてはいけません。そのようなクラスは、動的に生成して使用する場合のみ、shared_ptrで参照するようにします。