LoginSignup
5
4

More than 3 years have passed since last update.

C++でprivate readwriteかつpublic readonlyなプロパティもどき

Last updated at Posted at 2019-10-05

概要

C#のpublic int val { get; private set; }; をC++に実装したい話です。

忙しい人は下記コードだけ読んで察してください。

property.hpp
#include <type_traits>

template<class Owner>
struct property_enabled {
    template<typename T>
    struct rwro {
        private: T t;
        public:  template<class... Args> rwro(Args&&... args): t(args...) {}
        public:  operator const T& () const { return t; } // getter
        private: T& operator=(T&& t) { return (this->t = t); } // setter
        friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
    };
};

// usage
//struct Hoge : public property_enabed<Hoge> {
//     rwro<int> val; // ≒ public int val { get; private set; };
//};

モチベーション

C++でクラスを書いてると、メンバ変数をreadonlyで公開したいことがよくあります。
たとえば、

struct Hoge {
private:
    int val;
public:
    Hoge() { val = 1; }
    int get_val() { return val; } // getter
private:
    void set_val(int v) { val = v; } // setter。流儀によっては書かない
}

int main() {
    Hoge hoge;
    int a = hoge.get_val(); // ok
    hoge.set_val(2);        // error(クラス外から値を変更したくないのでエラーで問題ない)
}

しかしこれはvalとget_val() / set_val()を別々に定義する必要がありめんどくさいですし、
public側でhoge.valではなくhoge.get_val()なる関数を呼ぶような書き方をするのも気持ち悪い。

あと、たとえば仕様変更でvalがlongに変更になった場合にget_val()の型を変え忘れるなど、バグの温床にもなりかねません。

この問題、C#だとプロパティを使うだけでよくて、以下のように

// C#
class Hoge {
    public int val { get; private set; };
};

と定義するだけなんですよね1。get; private set;て。なにそれすごい。

これに似た機能をC++で書けるようにしたい。

(プロパティ自体はもっといろんな機能がありますが、あまり欲張るとろくな事がないので、ここではpublic get; private set; なプロパティだけを目指します)

先行事例

私が住むHPC畑ではC#なんて書く人が周りに居ないのでマイナーなんですが、
先人のC++使いの中には似たようなことをやりたい人がそこそこ居るらしく、
「C++ プロパティ」でぐぐると結構な数の記事が出てきます。

(たとえば2 3 4 5 6

テンプレートとかめちゃくちゃ駆使して任意のgetter/setterを指定したプロパティとか作ってる人もいてめちゃくちゃすごいです(語彙力)

しかしながら、

struct Hoge {
private:
    int _val;
public:
    Property<int> val;
    Hoge(): _val(val) {}
};

みたいにメンバ変数の実体とプロパティクラスを別々に定義する実装になっていて、違うそうじゃないgetter/setterもアクセス修飾も一意で良いから1行で書きたいんじゃ……というお気持ちです。

実装したコード

C++11を前提で書いています。

コンパイラは手元にあったGCC(7.4.0)を使用。

temlpate + friendで書いてみる

変数をプロパティ化するラッパークラス書いてfriend宣言すれば出来るのでは?と思い立ったのでここから開始。

template<typename T, class Owner>
struct rwro { // private read/write, public readonly.の略のつもり。命名のセンスください……
private:
    T t;
public:
    // コンストラクタ。とりあえずT(...)をたらい回しにしておく
    temalpte<class... Args> rwro(Args&&... args): t(args...) {}
public: 
    // いわゆるgetter。変数のように読みたいので、(暗黙な)キャスト演算子をオーバーロード
    operator const T& () const { return t; }
private: 
    // いわゆるsetter。変数のように代入したいので(ry
    T& operator=(T&& t) { return (this->t = t); }
private:
    // friend指定してoperator=を見えるようにする
    friend Owner;
};

使い方は下記

struct Hoge {
public:
    rwro<int, Hoge> val;
public:
    Hoge() { val = 1; }
};

int main() {
    Hoge hoge;
    int a = hoge.val; // ok
    hoge.val = 2;     // error(クラス外から値を変更したくないのでエラーで問題ない)
}

get_val() / set_val()が消えてすっきりしましたね。

hoge.valの実体はrwro<int, Hoge>なんですが、ぱっと見int valと同じようにアクセスできて良きです。

CRTPを使ってみる

上の例でもそこそこ良いんですが、メンバ変数を増やすたびにrwro<float, Hog> val2; みたいに毎回Hogeを書く必要があります。

めんどくさいしタイポしそう…

この問題、rwroからHogeが見えればわざわざHogeをテンプレート引数に入れる必要はなくなるわけでですね。
CRTP(Curiously Recurring Template Pattern)7 8 を使いましょう。

template<class Owner>
struct property_enabled {
    template<typename T>
    struct rwro {
        /// 中略
    };
};

こう使う。

struct Hoge: public property_enabled<Hoge> {
public:
    rwro<int> val;
    /// 以下略
};

1行目がCRTPというやつで、自分自身(Hoge)をテンプレート引数として渡したproperty_enabledを継承することで、
rwroからHogeが見えるようになり、friend指定ができるということになります。

これでメンバ変数が増えてもrwro val2; のように型Tを渡すだけなので楽になりましたね。

friendまわりのコンパイルエラー回避

friend Owner; でエラーが出る場合があります。

Ownerがクラスではないと判定されている?常にHogeみたいなクラスしか渡してないので大丈夫なはずなんですが……

よくわからないけどSFINAEにすればエラーは出ないでしょう。

#include <type_traits>

template<class Owner>
struct property_enabled {
    template<typename T>
    struct rwro {
        /// 中略
        friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
    };
};

これで現状最新形。おそらく基本型(intとかfloatとか)なら問題なく使えます。

メンバ変数のメンバを参照出来ない問題(未解決)

std::vectorとか、
メンバ変数(より正確にはプロパティ内のrwro::T t)のメンバにアクセス出来ない問題。

これはドット演算子が暗黙キャストを行わないからで、rwro::Tではなくrwroのメンバを参照しようとしてundefinedになるためですね。

struct Hoge : public property_enabled<Hoge> { 
    rwro<std::vector<int>> val;
};
hoge.val.size(); // 我々が真に求めていたもの(コンパイルエラーが出る)
static_cast<decltype(hoge.val.t)>(hoge.val).size()(); // C++が我々に与えたもの。これはひどい……

ちなみにドット演算子だけでなく、暗黙キャストを行わない任意の演算子で同じ問題が発生します。(添字演算子とか)

hoge.val[0]; // これもコンパイルエラー……

この問題、5年前の記事5でも言及されているんですが、コメント欄みてもぐぐっても解決策が出てこない辺り、無理なのかもしれません。

私ももう半分諦めていて、陽にconst T&とかを返すメンバ関数かoperatorを定義しようかなあと考えているところです。

template<class Owner>
struct property_enabled {
    template<typename T>
    struct rwro {
        /// 中略
        public:  const T& operator()() const { return t; } // hoge.val().size()
        private: T& ref() { return t; } // hoge.val.ref().push_back()
    };
};

まあ動かないことはないんですが、「変数のようにアクセスできる」という当初の目的は未達ですね。

あとpublic const T&とprivate T&で同じoperator使えないのが地味に辛い。

だれか解決策ください。

参考


  1. プロパティ - C# によるプログラミング入門 | ++C++; // 未確認飛行 C https://ufcpp.net/study/csharp/oo_property.html#level 

  2. C++でプロパティを実現するもっとも簡単な方法 | MaryCore https://marycore.jp/prog/cpp/simple-property/ 

  3. Set / Get とプロパティ - プログラミング雑記 | ++C++; // 未確認飛行 C https://ufcpp.net/study/miscprog/accessor.html 

  4. C++でプロパティ(Qiita) https://qiita.com/DandyMania/items/78bb31492bee095bc4b0 

  5. C++でProperty(getter/setter)(Qiita) https://qiita.com/HogeTatu/items/1bb3a394f88ba90cd37e 

  6. # C++でプロパティーを実現する方法 https://qiita.com/m5knt/items/1da740db9c1b1935f304 

  7. C++テンプレートイディオム CRTP - 右上➚ http://agtn.hatenablog.com/entry/2016/06/16/192708 

  8. Curiously Recurring Template Pattern (CRTP) と Policy-based design - meryngii.neta http://meryngii.hatenablog.com/entry/2016/12/22/210753 

5
4
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4