C++でプロパティを実現するココロミ

CodeZine / 2014年1月28日 14時0分

 昨年末からC++/CXで遊んでいます。C++/CXは"ほぼC++"ではありますが、C++にはない機能も散見されます。GC(garbage collection)のおかげでnewしたものの後始末は勝手にやってくれますし、あらゆるものがPlatform::Objectから(直接/間接的に)導出されているのも特徴の一つです。今回のお題はC++にはない機能の一つ:プロパティをC++で実現したらどうなるの、ってオハナシ。

■C++/CXのプロパティ

 C++/CXあるいはC++/CLI、C#、VB.NETのプロパティとは、あたかも変数であるかのようにふるまう関数です。例えば:

list-01
using namespace Platform; ref class Person sealed { public: property String^ Name { String^ get(); void set(String^ value); } property int Age { int get(); void set(int value); } private: String^ name_; int age_; }; String^ Person::Name::get() { return name_; } void Person::Name::set(String^ value) { name_ = value; } int Person::Age::get() { return age_; } void Person::Age::set(int value) { age_ = value; }
 このように、property名前に対しget/set関数を定義することで、あたかもメンバ変数であるかのごとくふるまいます。

list-02
int main(Platform::Array<Platform::String^>^) { Person man; // メンバ変数への値の代入のようにふるまう man.Name = L"Adam"; man.Age = 21; // メンバ変数の持つ値を読みだしているかのようにふるまう String^ name = man.Name; int age = man.Age; Details::Console::WriteLine(L"Name : " + name); Details::Console::WriteLine(L"Age : " + age.ToString()); }
 man.Name、man.Ageが左辺値と解釈されるならset(),右辺値と解釈されるならget()が呼び出されます。変数の読み書きに見せかけた関数の呼び出しです。これをC++でも実現した例がないかと探してみたら、ありました。C++標準員会:SC22/WG21にプロポーザル(提案書):N1615として提出されていました。十数ページの小さなドキュメントですし、英語の勉強がてら読んでみるのもよいでしょう。N1615はプロパティを言語仕様の拡張によってではなく、ライブラリで実現する方法を提案しています。そこにはこんな小さなコードがありました。

list-03
/* excerpt from * SC22/WG21/N1615 "C++ Properties -- a Library Solution" 2004-04-09 */ template<class T, class Object, typename T(Object::*real_getter)() const, typename T(Object::*real_setter)(T const&)> class RWProperty { Object* my_object; public: void operator()(Object* obj) { my_object = obj; } T get() const { return (my_object->*real_getter)(); } T set(T const& value) { return (my_object->*real_setter)(value); } T operator()() const { return get(); } T operator()(T const& value) { return set(value); } operator T() const { return get(); } T operator=(T const& value) { return set(value); } typedef T value_type; };
 このRWPropertyを使って作られたPersonがコチラ。

list-04
#include <iostream> #include <string> class Person { private: int age_; // getter/setter for age int get_age() const { return age_; } int set_age(int const& value) { return age_ = value; } std::string name_; // getter/setter for name std::string get_name() const { return name_; } std::string set_name(std::string const& value) { return name_ = value; } public: Person() { Age(this); Name(this); /* do this before use! */ } // property age/name RWProperty<int, Person, &Person::get_age, &Person::set_age > Age; RWProperty<std::string, Person, &Person::get_name, &Person::set_name> Name; ostream& print_on(std::ostream& out) const { return out << name_ << ':' << age_; } }; std::ostream& operator<<(std::ostream& stream, const Person& p) { return p.print_on(stream); } using namespace std; int main() { Person adam; adam.Name = "Adam"; adam.Age = 20; cout << adam << endl; string name = adam.Name; int age = adam.Age; cout << name << ':' << age << endl << endl; }
 ...なるほど。プロパティの型とプロパティの持ち主の型、そしてget/setするメンバ関数をテンプレート引数に与え、持ち主のコンストラクタ内でthisで初期化しておけば、man.Ageが右辺値ならoperator int(),左辺値ならoperator=(int const&)の中から(テンプレート引数で与えておいた)get/set関数が呼び出されるってカラクリですな。関数へのポインタはコンパイル時に決定する定数なのでテンプレート引数に与えることができ、RWPropertyのメンバは持ち主のポインタだけで済ませています。

 このプロポーザル、2004年に提出されています。2004年というとC++03が決まったばかり。テンプレートの仕様も今ほどにはきっちり定まっていない時期としては妥当な実装ではないかと思います。要はプロパティが右辺/左辺に現れたとき、あらかじめ用意しておいた関数が呼ばれればいい。この実装をお手本に"イマ風"に書き直してみましょうか。



CodeZine

トピックスRSS

ランキング