この記事はC++ Advent Calendar 2019 19日目の記事です。
昨日は@shohiroseさんの「メッシュクラスを自作した」でした。
明日は@yumetodoさんが担当です。
はじめに
C#を使ってて久しぶりにC++に戻ってきて思ったこと……
プロパティを使いたい!
ということで調べたところ、やはり同じことを考えている方は多くいらっしゃるようで、すでにいくつかの実装が存在しました。
だいたいの実装がコンストラクタにラムダ式などでgetter, setterを渡すようになっています。
ところで、C++の関数やメンバ関数はコンパイル時にアドレスが決まるため、非型テンプレート引数に渡すことができます。
せっかくC++なんだからできるだけコンパイル時に設定できるようにしたいです。ということで、メンバ関数のこの性質を利用して、getterやsetterの指定をテンプレートパラメータで行うことができるプロパティを作ったので、紹介します。
この記事で紹介するプロパティでできることはこんな感じです。
- 普通のプロパティ
- getter-onlyプロパティ
- setter-onlyプロパティ
- 仮想getterと仮想setter
- 仮想getterや仮想setterのオーバーライド
逆にできないことはこんなんです。
- staticなプロパティ
- 自動プロパティ
動作確認済みコンパイラ
以下のコンパイラにC++17オプションを付けてコンパイルできます。
- clang 8.0.0
- g++ 8.3.0
- MSVC 14.2 (Visual Studio 2019)
C#のプロパティとは
C++やJava、その他さまざまな言語では、メンバ変数を外部から直接触られないように、メンバ変数はprivateにして、publicなアクセス用のメンバ関数を用意します。
そのようなメンバ関数のうち、メンバ変数取得用のメソッドをgetter、設定用のメソッドをsetterと呼びます。
このgetterとsetterをまとめて、変数のように扱えるようにしたのがC#のプロパティです。
だいぶ雑な説明ですが、詳細は以下のサイトを見てください。
C#のプロパティにはgetter, setterの有無によって以下の三種類存在します。
- getterのみが定義されたプロパティ: 外部からは値の参照のみ可能
- setterのみが定義されたプロパティ: 外部からは値の設定のみ可能
- getterとsetterが定義されたプロパティ: 外部から値の読み書きが可能
これら三つについて、C++で再現したクラスを作成しました。
準備
プロパティクラスで使用しているものについて説明します。
メンバ関数ポインタからクラスの型を取得するエイリアステンプレート
プロパティクラスのテンプレート引数に渡されるgetterやsetterのメンバ関数ポインタから、クラスの型を特定するのに使用します。
namespace static_property {
template <class C, typename TReturn, typename... TArgs>
auto member_function_class_type_impl(TReturn (C::*member_function)(TArgs...) const) -> C;
template <class C, typename TReturn, typename... TArgs>
auto member_function_class_type_impl(TReturn (C::*member_function)(TArgs...)) -> C;
template <auto MemberFunctionPointer>
using member_function_class_type_t = decltype(member_function_class_type_impl(MemberFunctionPointer));
ここでは、C++17から入った「非型テンプレートパラメータのauto宣言(Declaring non-type template arguments with auto)」を使用しています。
まず、member_function_class_type_t
エイリアスで非型テンプレートパラメータのauto
によりメンバ関数ポインタを受け取り、それをテンプレート関数member_function_class_type_impl
の引数に渡しています。
member_function_class_type_impl
関数は、テンプレート引数推論によりメンバー関数が定義されているクラスを特定し、そのクラスを戻り値の型とします。
member_function_class_type_impl
関数の戻り値型をdecltype
で得て、その型のエイリアスをmember_function_class_type_t
とすることで、最終的にはmember_function_class_type_t
にテンプレート引数で渡されたメンバ関数ポインタの定義されているクラス型が設定されます。
constメンバ関数と非constメンバ関数に対応するために、member_function_class_type_impl
はconst版と非const版の二つ定義しています。
読み取り専用プロパティ
まずはgetterのみを定義可能なプロパティクラスを紹介します。
コード
上で説明したメンバ関数ポインタからクラスの型を取得するメタ関数を使っているおかげで、getterとなるメンバ関数ポインタからクラス型を取得できるので、わざわざクラスをテンプレート引数として渡す必要がありません。
……まだ記事の初めですが、ここが一番のこだわりポイントで、これ以外特に面白い部分がないです。
残りは自身の状態を変化させない演算子をずらっと定義しています。C++演算子多過ぎ問題。
template <typename TProperty, auto Getter>
class get_only_property {
using C = member_function_class_type_t<Getter>;
public:
get_only_property(const C& ins) : _instance(ins) {}
//プロパティ型への変換
operator TProperty() const { return get(); }
/*()を用いたアクセス*/
TProperty operator()() const { return get(); }
//配列添え字演算子
template <typename T>
auto operator[](const T& other) const
-> decltype(std::declval<TProperty>[other]) {
return get()[other];
}
//四則演算子
template <typename T>
auto operator+(const T& other) const
-> decltype(std::declval<TProperty>() + other) {
return get() + other;
}
template <typename T>
auto operator-(const T& other) const
-> decltype(std::declval<TProperty>() - other) {
return get() - other;
}
template <typename T>
auto operator*(const T& other) const
-> decltype(std::declval<TProperty>() * other) {
return get() * other;
}
template <typename T>
auto operator/(const T& other) const
-> decltype(std::declval<TProperty>() / other) {
return get() / other;
}
template <typename T>
auto operator%(const T& other) const
-> decltype(std::declval<TProperty>() % other) {
return get() % other;
}
//ビット演算子
template <typename T>
auto operator|(const T& other) const
-> decltype(std::declval<TProperty>() | other) {
return get() | other;
}
template <typename T>
auto operator&(const T& other) const
-> decltype(std::declval<TProperty>() & other) {
return get() & other;
}
template <typename T>
auto operator^(const T& other) const
-> decltype(std::declval<TProperty>() ^ other) {
return get() ^ other;
}
//論理演算子
template <typename T>
bool operator||(const T& other) const {
return get() || other;
}
template <typename T>
bool operator&&(const T& other) const {
return get() && other;
}
//シフト演算
template <typename T>
TProperty operator>>(const T& other) const {
return get() >> other;
}
template <typename T>
TProperty operator<<(const T& other) const {
return get() << other;
}
//比較演算子
template <typename T>
bool operator==(const T& other) const {
return get() == other;
}
template <typename T>
bool operator!=(const T& other) const {
return get() != other;
}
template <typename T>
bool operator>(const T& other) const {
return get() > other;
}
template <typename T>
bool operator>=(const T& other) const {
return get() >= other;
}
template <typename T>
bool operator<(const T& other) const {
return get() < other;
}
template <typename T>
bool operator<=(const T& other) const {
return get() <= other;
}
//単項演算子
TProperty operator+() const { return +get(); }
TProperty operator-() const { return -get(); }
TProperty operator!() const { return !get(); }
TProperty operator~() const { return ~get(); }
protected:
TProperty get() const { return (_instance.*Getter)(); }
private:
const C& _instance;
};
使い方
まるでC#でプロパティを使っているかのような使い心地です。
class Test {
private:
int _value{777};
int get_value() const { return _value; }
public:
//プロパティ型とgetterとなるconstメンバ関数をテンプレート引数に渡す。コンストラクタには自クラスの参照を渡します
static_property::get_only_property<int, &Test::get_value> get_only_value{
*this};
};
int main() {
Test test;
//普通のメンバ変数のように値を参照できます
std::cout << test.get_only_value << std::endl;
//当然四則演算ができます
std::cout << test.get_only_value + 123 << std::endl;
//当たり前のように単項演算子も使えます
std::cout << -test.get_only_value << std::endl;
//まあプロパティ同士の掛け算もできます
std::cout << test.get_only_value * test.get_only_value << std::endl;
//比較もできちゃいます
std::cout << (test.get_only_value == 777) << std::endl;
//()演算子で明示的にgetterを呼び出してプロパティの値を取得することもできます
auto value = test.get_only_value();
std::cout << value << std::endl;
}
777
900
-777
603729
1
777
書き込み専用プロパティ
続いて、setterのみを設定できる書き込み専用プロパティクラスを紹介します。
コード
定義するべき演算子が少ないので、get_only_property
よりもだいぶすっきりしています。
特筆するべきことは特にないです。
template <typename TProperty, auto Setter>
class set_only_property {
using C = member_function_class_type_t<Setter>;
public:
set_only_property(C& ins) : _instance(ins) {}
//代入
set_only_property& operator=(const TProperty& v) {
set(v);
return *this;
}
/*()を用いたアクセス*/
void operator()(const TProperty& v) { set(v); }
protected:
void set(const TProperty& v) { (_instance.*Setter)(v); }
private:
C& _instance;
};
使い方
もはやC#としか思えない使い心地です。
class Test {
private:
int _value{777};
void set_value(const int& v) {
std::cout << "セットされた!!!: " << v << std::endl;
_value = v;
}
public:
//プロパティ型とsetterとなるメンバ関数をテンプレート引数に渡す。コンストラクタには自クラスの参照を渡します
static_property::set_only_property<int, &Test::set_value> set_only_value{
*this};
};
int main() {
Test test;
//普通の変数のように値を代入できます
test.set_only_value = 464949;
//こちらも明示的にセッターを呼び出して代入することもできます
test.set_only_value(634);
}
セットされた!!!: 464949
セットされた!!!: 634
読み書き可能プロパティ
読み込み、書き込みどちらもできてしまうプロパティクラスです。
コード
上で載せたget_only_property
とset_only_property
を継承し、複合代入演算子のオーバーロードを追加しています。
template <typename TProperty, auto Getter, auto Setter>
class get_set_property : public get_only_property<TProperty, Getter>,
public set_only_property<TProperty, Setter> {
using C = member_function_class_type_t<Getter>;
static_assert(std::is_same_v<C, member_function_class_type_t<Setter>>,
"The class of Getter and Setter must be same.");
using TGetProperty = get_only_property<TProperty, Getter>;
using TSetProperty = set_only_property<TProperty, Setter>;
public:
get_set_property(C& ins) : TGetProperty(ins), TSetProperty(ins) {}
//代入
get_set_property& operator=(const TProperty& v) {
TSetProperty::operator=(v);
return *this;
}
//前置インクリメント
get_set_property& operator++() {
auto buf = TGetProperty::get();
TSetProperty::set(++buf);
return *this;
}
//後置インクリメント
TProperty operator++(int) {
auto buf = TGetProperty::get();
auto ret = buf++;
TSetProperty::set(buf);
return ret;
}
//前置デクリメント
get_set_property& operator--() {
auto buf = TGetProperty::get();
TSetProperty::set(--buf);
return *this;
}
//後置デクリメント
TProperty operator--(int) {
auto buf = TGetProperty::get();
auto ret = buf--;
TSetProperty::set(buf);
return ret;
}
//複合代入演算子(四則演算)
template <typename T>
get_set_property& operator+=(const T& other) {
auto buf = TGetProperty::get();
TSetProperty::set(buf += other);
return *this;
}
template <typename T>
get_set_property& operator-=(const T& other) {
auto buf = TGetProperty::get();
TSetProperty::set(buf -= other);
return *this;
}
template <typename T>
get_set_property& operator*=(const T& other) {
auto buf = TGetProperty::get();
TSetProperty::set(buf *= other);
return *this;
}
template <typename T>
get_set_property& operator/=(const T& other) {
auto buf = TGetProperty::get();
TSetProperty::set(buf /= other);
return *this;
}
template <typename T>
get_set_property& operator%=(const T& other) {
auto buf = TGetProperty::get();
TSetProperty::set(buf %= other);
return *this;
}
//複合代入演算子(ビット演算)
template <typename T>
get_set_property& operator|=(const T& other) {
auto buf = TGetProperty::get();
TSetProperty::set(buf |= other);
return *this;
}
template <typename T>
get_set_property& operator&=(const T& other) {
auto buf = TGetProperty::get();
TSetProperty::set(buf &= other);
return *this;
}
template <typename T>
get_set_property& operator^=(const T& other) {
auto buf = TGetProperty::get();
TSetProperty::set(buf ^= other);
return *this;
}
//複合代入演算子(シフト演算)
template <typename T>
get_set_property& operator>>=(const T& other) {
auto buf = TGetProperty::get();
TSetProperty::set(buf >>= other);
return *this;
}
template <typename T>
get_set_property& operator<<=(const T& other) {
auto buf = TGetProperty::get();
TSetProperty::set(buf <<= other);
return *this;
}
};
使い方
C#を超えてC++++++くらいの使い心地です。
class Test {
private:
int _value{777};
int get_value() const { return _value; }
void set_value(const int& v) {
std::cout << "セットされた!!!: " << v << std::endl;
_value = v;
}
public:
//プロパティ型とgetterとなるconstメンバ関数、setterとなるメンバ関数をテンプレート引数に渡す。コンストラクタには自クラスの参照を渡します
static_property::get_set_property<int, &Test::get_value, &Test::set_value> get_set_value{
*this};
};
int main() {
Test test;
//set_only_propertyのように値の設定ができて
test.get_set_value = 4946;
//get_only_propertyのように値の参照ができるだけでなく
std::cout << test.get_set_value << std::endl;
//加算代入やシフト代入などもできてしまう
test.get_set_value += 3;
std::cout << test.get_set_value << std::endl;
test.get_set_value <<= 10;
std::cout << test.get_set_value << std::endl;
}
セットされた!!!: 4946
4946
セットされた!!!: 4949
4949
セットされた!!!: 5067776
5067776
まとめ
いくらC#に近づけようとしても、C++はC++です。それぞれに利点と欠点があるのだから、C++をC#に近づけようとして無理にプロパティを使う必要性はないと思いました。
というより単純に詰め切れていないので、実際に使うには考慮不足であったり不足していたりする点があると思います。そのような点を見つけた方は、コメントしていただけると嬉しいです。
ちなみに、今回紹介した方法では、getter, setterをメンバ関数で指定するのが前提になっています。このため、staticのプロパティには対応していません。また、C#ではよく使う自動プロパティにも対応していません。
この二つは今回の方法を応用すれば比較的簡単に実装できそうなので、機会があればそれらも含めてまた記事を書こうと思っています。