C++でプロパティを実現するもっとも簡単な方法

C++には今流行のプロパティ(property)という機能はありませんが、参照の仕組みを用いることでプロパティと同等の機能を実現することが可能です。後半では応用例として、より現実的なプロパティの作り方を紹介していきます。

プロパティの簡単な作り方

struct Person {
  const char* _name;
  const char*& name() { return _name; }
};
int main() {
  Person person = {"tom"};
  puts(person.name()); // "tom"
  person.name() = "bob";
  puts(person.name()); // "bob"
}

参照型の戻り値に対する代入処理が印象的です(person.name() = "bob";)。この場合、参照先の_name変数はきちんと書き換わります。

このようにメンバ関数name()が単体でアクセサ(ゲッター/セッター = getter/setter)の両者の役割を担っている点が本イディオムの特徴です。これによって一般的なプロパティと相違ない機能を実現することが可能となります。

ただ、アクセサ側で振る舞いを持たせるようなことが出来ない所が難点です。例えば遅延初期化や排他処理のようなことが出来ません。

// 遅延初期化
const char* getName() { return _name ?: (_name = "lazy"); }
// 排他処理
void setName(const char* s) { if (s != NULL) _name = s; }

アクセサを制御する方法

アクセサに対して振る舞いを持たせたい場合は、アクセサをラップした特殊なヘルパークラスを作成することで、値の参照時と代入時の挙動を制御することが可能になります。

struct Name {
  struct Property {
    Name& r;
    // ゲッター(デフォルト処理 あり)
    operator const char*() { return r._name ?: "名無し"; }
    // セッター(null排他 あり)
    void operator=(const char* s) { if (s) r._name = s; }
  };
  const char* _name;
  Property name() { return {*this}; }
};
Name name = {nullptr};
puts(name.name()); // "名無し"
name.name() = "bob";
puts(name.name()); // "bob"
name.name() = nullptr;
puts(name.name()); // "bob"

Propertyというヘルパークラスに自身の参照を渡している点が特徴です。

puts(name.name());の際にはヘルパークラス側のキャスト演算子operator const char*()が働くため、ゲッターが機能することになります。

またname.name() = "bob";の際には代入演算子void operator=(const char* s)が働くため、セッターが機能します。

この発想はイテレータの発想にも少し似た所があるかもしれません。

関数呼び出しを無くす方法

先程のヘルパーを構造体のメンバ変数とし、そのヘルパー変数に対して代入と参照を行うことで、アクセサを間接的に呼び出すことも可能です。

これでC++でほぼ完全なプロパティ機能を実現することが可能になります。

struct Name {
  const char* _name;
  struct Property {
    Name& r;
    operator const char*() { return r._name ?: "名無し"; }
    void operator =(const char* s) { if (s) r._name = s; }
  };
  Property name = {*this};
};
Name name = {nullptr};
puts(name.name); // "名無し"
name.name = "bob";
puts(name.name); // "bob"
name.name = nullptr;
puts(name.name); // "bob"

本イディオムはより簡潔に記述することができます。

struct Name {
  const char* _name;
  struct {
    Name& r;
    operator const char*() { return r._name ?: "名無し"; }
    void operator=(const char* s) { if (s) r._name = s; }
  } name{*this};
};

汎用化

テンプレートを用いて汎用化することも可能です。

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

テンプレートに静的な関数を渡すよう改良すれば、Computed propertyの実現も可能になります。またはメンバとして関数オブジェクトを渡す方法も考えられます。

注意点

本イディオムは暗黙的な型変換の仕組みを利用しているため、値受け取り側の型が曖昧な場合には明示的な型キャストが必要になります。

auto p = name.age;      // Property<int>型の値が返ってくる
auto i = (int)name.age; // int型の値が返ってくる

printf("%i", name.age); // NO: Format specifies type 'int' but the argument has type 'Property<int>'
printf("%i", static_cast<int>(name.age)); // OK

なおこの場合、間接参照演算子やアロー演算子の多重定義を駆使して元の型の値を取得したり、メンバ関数を呼び出すことも可能になりますので参考までに。

struct Person {
  std::string _name;
  struct {
    Person& r;
    operator std::string() { return r._name; }
    void operator=(std::string s) { r._name = s; }
    std::string* operator->() { return &r._name; }
    std::string& operator *() { return  r._name; }
  } name{*this};
};

Person p{"bob"};
puts(p.name->c_str()); // "bob"
p.name = "tom";
std::cout << *p.name;  // "tom"
std::cout <<  p.name;  // エラー: Invalid operands to binary expression ('ostream' (aka 'basic_ostream<char>') and 'struct (anonymous struct)')

ライブラリ

プロパティー・クラス側で値を保持する方法もあります。

namespace mc {
  template<class T> struct property {
    T _v;
    operator T() { return _v; }
    void operator=(const T& v) { _v = v; }
    T* operator->() { return &_v; }
    T& operator *() { return  _v; }
    friend std::ostream& operator<<(std::ostream& os, const property& it) { return os << it._v; }
  };
}

struct Person {
  mc::property<const char*> firstName;
  mc::property<std::string> lastName;
  mc::property<int>         age;
};

int main() {
  Person p = {"tom", "yum", 16};
  std::cout << p.firstName << " ";
  std::cout << p.lastName->c_str();
  printf("(%d)\n", *p.age);
  
  p.firstName = "O", p.lastName = "B", p.age = 1;
  printf("%s%s(%d)\n", *p.firstName, p.lastName->data(), *p.age);
}

メンバ変数をproperty構造体でラップしていますが、最適化ビルド時にはゼロオーバヘッドによる動作が期待できます。

既存クラスのプロパティ対応

演算子オーバーロードを活用することで、既存クラスに対するプロパティ対応が可能になります。

template<class F> auto operator-(std::string& s, F f) { return f(s); }
template<class F> auto operator/(std::string& s, F f) { return f(s); }

struct string_size_property {
  std::string* _s = nullptr;
  string_size_property operator()(std::string& s) { return {&s}; }
  operator std::string::size_type() { return _s->size(); }
  std::string& operator=(std::string::size_type n) { return _s->resize(n), *_s; }
  friend std::ostream& operator<<(std::ostream& o, const string_size_property& p) { return o << p._s->size(), o; }
} size;
std::string str = "hello";
size_t i = str-size;
str-size = 4;
std::cout << i;        // 5
std::cout << str-size; // 4
std::cout << str;      // "hell"
std::cout << str/size * 2; // 8

詳しい解説については、以下のページが参考になります。

【C++】既存クラスを拡張する方法【拡張メソッド/カテゴリ】

広告