C++でC#ライクなプロパティを実現する

C#ではプロパティ構文というものがあります。これは、オブジェクトのメンバーへのアクセスを行うための、アクセッサー(setter/getter)をあたかも変数へのアクセスと同様に行える仕組みです。

// C#のプロパティ
public class CSharpProp
{
    // 一番簡単なプロパティの実装
    public int Number { get; private set; } = 30;

    // 代入する変数と処理をset/getに記述できる
    protected int _ID;
    public int ID { 
        get { return _ID; }
        set { _ID = value; }
    }
}

// プロパティを利用する場合
public static void Main(string[] args)
{
    CSharpProp p = new CSharpProp();
    // p.setNumber(10)と同じ処理になる。このprivateなので場合エラー
    p.Number = 10; 

    // 30と表示される。p.GetNumber(); と同じ処理になる。
    Console.WriteLine(p.Number);
}

このように変数の代入、取り出しでメソッド呼び出しになり、いちいちメソッド呼び出しする手間が削減できます。で、これをC++で実現しようという試みは、昔から色々と皆さん考えられているようなのですが、ある程度問題を解決できたかなと思うのでC++でプロパティを使うための方法を紹介したいと思います。

MSの独自拡張である、C++/CLI の property キーワードを使用するアイデアがありますが、それって、C++とは別の言語の話なので論外として、別の実現方法を考えたいと思います。

と、言いたいところですが、実は、以下サイト様にとても良い解決策があり、基本的に問題が解決しています。

// 参考サイト様より引用
// http://marycore.jp/prog/cpp/simple-property/

template<class T> struct Property // プロパティ構文を支援するクラス
{ 
  T& r;
  operator T() { return r; }
  void operator =(const T v) { r = v; }
};

struct Person // 実際にプロパティを宣言している箇所
{ 
  const char* _name;
  int _age;
  Property<const char*> name{_name};
  Property<int> age{_age};
};

// 使用すると以下のようになる
Person person = {"Shop", 9};
puts(person.name); // "Shop"
printf("%d", (int)person.age); // 9
person.age = 8;
printf("%d", (int)person.age); // 8

このコード凄いですね。このように、メソッド呼び出しなんだけどかっこを付けないで呼び出すことができ、使用感が C# のプロパティとほとんど同じ感じになります。

但し、上記の処理のみだと、set/getするときに任意の処理が行えず、結果的に変数を public: で公開しているのと大差ない状態となっています。そこで、上記クラスを拡張し任意の処理をset/get時に挿入できるように拡張したいと思います。

Propertyに任意の処理をできるよう拡張する

上記引用コードに対し、C++11の機能の std::function をラムダ式を使って set/get の処理を外部から指定できるように拡張します。

以下、VisualStudio2017のC++で実装しています。*1

// プロパティ構文を支援するクラス
template<class T> class Property
{
public:
    T& r;
    std::function<void(T value)> set = nullptr; // ★追加
    std::function<T()> get = nullptr; // ★追加

    operator T()
    {
        // getが設定されてれば登録されたほうを呼ぶ
        return get ? this->get() : this->r;
    }

    // 直接中身を参照できるようにアロー演算子もオーバーロード
    T operator ->()
    {
        return get ? this->get() : this->r;
    }

    void operator =(const T v)
    {
        if (set) // setが設定されていれば登録されたほうを呼ぶ
        {
            this->set(v);
        }
        else
        {
            r = v;
        }
    }
};

プロパティを使ったクラス定義と使用方法は以下の通りです。

 // 実際にプロパティを宣言している箇所
class Sample
{
    int _a, _b, _c;
    Sample* _sample;
public:

    // 何も設定しない
    Property<int> a{ _a };

    // setだけ指定
    Property<int> b{ _b, [this](int v) { _b = v * 2;/*倍で保存*/ }, nullptr };

    // getだけ指定
    Property<int> c{ _c, nullptr, [this]() { return _c * 3;/*倍で取得*/ } };

    // オブジェクトへの参照
    Property<Sample*> sample { _sample };
};

// 使用すると以下のようになる
Sample sample;
sample.a = 10;
cout << sample.a << endl; // 10
sample.b = 20;
cout << sample.b << endl; // 40
sample.c = 30;
cout << sample.c << endl; // 90

// アロー演算子のオーバーロードによりget後に以下のように同じ行でメンバー参照ができる
sample.sample->xxx();

色々やっているので、パフォーマンスが悪くなるかもしれませんがあまり気にしないようにします。

Cocos2d-x の CC_SYNTHESIZE_RETAIN を置き換える

以下、ゲーム作成ライブラリのCocos2d-xの話です。

Cocos2d-xでは、get/setのアクセッサーを自動で生成する CC_SYNTHESIZE というマクロがあります。定義内容は以下の通り。

// 定義
#define CC_SYNTHESIZE(varType, varName, funName)

// --- 展開される処理の内容 ---
protected: varType varName; // 変数
public: virtual inline varType get##funName(void) const { return varName; } // getter
virtual inline void set##funName(varType var) { varName = var; } // setter

で、私事ですが、Cocos2d-x の実装をVisualStudioで行っている関係で、このマクロを使用すると、以下のインスペクションがずっと表示されて大変うっとおしいんですよね…マクロの使用感もちょっと微妙ですし。

f:id:Takachan:20180728214257p:plain

なので今回作成したプロパティでCC_SYNTHESIZEを置き換えようと思います。

// CC_SYNTHESIZE を使ったコード
class Character : public cocos2d::Ref
{
    CC_SYNTHESIZE(int, _age, age);  // 生存期間
    CC_SYNTHESIZE(float, _x, x); // モデル上の現在位置
    CC_SYNTHESIZE(float, _y, y);
    CC_SYNTHESIZE(float, _width, width); // モデルの大きさ
    CC_SYNTHESIZE(float, _height, height);
    CC_SYNTHESIZE(Angle, _angle, angle); // 向いている方向

public:
    // 以下略

書き換えたのが以下のコード

// プロパティに書き換えたコード
class Character : public cocos2d::Ref
{
    int _age;  // 生存期間
    float _x; // モデル上の現在位置
    float _y;
    float _width; // モデルの大きさ
    float _height;
    Angle _angle; // 向いている方向

public:
    Property<int> age{ age };
    Property<int> x{ x };
    Property<int> y{ y };
    Property<int> width{ width };
    Property<int> height{ height };
    Property<Angle> angle { _angle };
};

CC_SYNTHESIZE_RETAINを置き換える

次は、CC_SYNTHESIZE_RETAINをプロパティ構文に置き換えたいと思います。このマクロはsetter/getterを展開するとともにsetされたときに参照カウントを+1、参照が外れる時に-1される処理を含みます。マクロの実装は以下の通りです。

// 定義
#define CC_SYNTHESIZE_RETAIN(varType, varName, funName)

// --- 展開される処理の内容 ---
protected: varType varName; // 変数
public: virtual inline varType get##funName(void) const { return varName; } // getter
virtual inline void set##funName(varType var)
{
    if (varName != var)
    {
        CC_SAFE_RETAIN(var); // 参照カウントを+1
        CC_SAFE_RELEASE(varName); // 既存のRef があれば参照カウントを-1
        varName = var;
    }
} 

自分のC++のレベルだと、先ほどのプロパティ構文を直接拡張する方法では実現できそうにないため、Refオフジェクト専用のPropertyとして、「PropertyRef」を作成します。

// CC_SYNTHESIZE_RETAINのプロパティ版
template<class T> class PropertyRef
{
public:
    T& r;
    std::function<void(T value)> set = nullptr;
    std::function<T()> get = nullptr;

    operator T()
    {
        return get ? this->get() : *this->r;
    }

    T operator ->()
    {
        return get ? this->get() : this->r;
    }

    void operator =(const T v)
    {
        if (r != nullptr) 既存のRef があれば参照カウントを-1
        {
            r->release();
            r = nullptr;
        }

        v->retain();

        if (set)
        {
            this->set(v);
        }
        else
        {
            r = v;
        }
    }
};

マクロと同じように、プロパティへオブジェクトを追加すると参照カウントが+1され、他のオブジェクトやnullptrが再設定されると以前のオブジェクトの参照カウントを-1するように処理を拡張しました。

上記メソッドの使用方法は以下の通りです。

class Sample
{
    // 変数宣言
    cocos2d::Sprite* _sp = nullptr;

public:

    // Ref 派生クラス専用のプロパティ
    PropertyRef Sprite{ _sp };
};

こうすることによって、プロパティに対し以下操作で参照カウントが増減します。

Sample p;
// 参照カウントが+1される => 解放されなくなる
p.Sprite = Sprite::create("img.bmp");
// メンバーの呼び出し
p.Sprite->setZIndex(10);
// null代入で参照カウント-1となり解放される
p.Sprite = nullptr;

余談ですが、プロパティへの代入時にreleaseが走った時にエラーになる場合、_sp = nullptr のようにフィールドの初期化をどこかでやらないとエラーが起きるかもしれません。(PODのデフォルト初期化関係で未割当だとクラッシュします。

何回か改善した結果、大体マクロと同程度くらいのシンプルさになったと思います。この考え方自体は通常のC++でも使用できるので、ゲームエンジン以外にも役に立つかもしれません。

*1:MSVCでGCCと動きが違うとかあったらすいません