はじめに
C/C++から離れ、しばらくC#やPythonなどの言語で開発してから戻ってくると
プロパティが使えないので、Setter/Getterなどのインターフェース追加したりする。
ただ、これもパラメータの数が増えてくると追加や動作確な認に時間を取られるのも悲しいので
テンプレートを作ってC#などのようにプロパティっぽく扱えないか試してみた。
プロパティ用テンプレート
Property.hpp
#define _USE_MATH_DEFINES
#include <iostream>
#include <functional>
#include <cmath>
#include <algorithm>
template <typename T>
class Property {
private:
T value{};
std::function<T(const T&)> setter;
std::function<T()> getter;
public:
Property() = default;
Property(const T& val) : value(val) {}
// 両方指定するコンストラクタ
Property(std::function<T()> get_fn, std::function<T(const T&)> set_fn)
: getter(get_fn), setter(set_fn) {}
// コンストラクタ: getter のみ設定(Read-Only)
Property(std::function<T()> get_fn)
: getter(get_fn), setter(nullptr) {}
// コンストラクタ: setter のみ指定
Property(std::function<T(const T&)> set_fn)
: setter(set_fn) {}
// 初期値 + setter だけを指定するコンストラクタ
Property(const T& initial_val, std::function<T(const T&)> set_fn)
: setter(set_fn)
{
// 初期値も Setter(フィルター)を通す
if (setter) {
value = setter(initial_val);
} else {
value = initial_val;
}
}
// 代入演算子 (=)
Property& operator=(const T& val) {
if (setter) {
// フィルター関数を通して安全に内部の value にセット
value = setter(val);
} else {
value = val;
}
return *this;
}
// 型変換演算子 (評価/Get)
operator T() const {
if (getter) {
return getter();
}
return value;
}
// --- ストリーム出力演算子 (std::cout << prop 用)
friend std::ostream& operator<<(std::ostream& os, const Property& prop) {
os << static_cast<T>(prop);
return os;
}
// --- 複合代入演算子 ---
Property& operator+=(const T& val) {
*this = static_cast<T>(*this) + val;
return *this;
}
Property& operator-=(const T& val) {
*this = static_cast<T>(*this) - val;
return *this;
}
Property& operator*=(const T& val) {
*this = static_cast<T>(*this) * val;
return *this;
}
Property& operator/=(const T& val) {
*this = static_cast<T>(*this) / val;
return *this;
}
// --- インクリメント / デクリメント ---
// 前置 (++prop) : 変更後の値を返す
Property& operator++() {
*this += 1;
return *this;
}
// 後置 (prop++) : 変更前の値を返す (intのダミー引数が必要)
T operator++(int) {
T old_val = static_cast<T>(*this);
*this += 1;
return old_val;
}
// 前置 (--prop)
Property& operator--() {
*this -= 1;
return *this;
}
// 後置 (prop--)
T operator--(int) {
T old_val = static_cast<T>(*this);
*this -= 1;
return old_val;
}
};
// ---- 角度に特化した拡張プロパティ
template <typename T = double>
class PropertyDeg : public Property<T> {
public:
using Property<T>::Property;
PropertyDeg& operator=(const T& val) {
Property<T>::operator=(val);
return *this;
}
// 角度(Degree)をそのまま返す
T ToDeg() const {
return static_cast<T>(*this);
}
// ラジアン(Radian)に変換して返す
T ToRad() const {
return ToDeg() * static_cast<T>(M_PI / 180.0);
}
};
// ---- 長さに特化した拡張プロパティ
template <typename T = double>
class PropertyLength : public Property<T> {
public:
using Property<T>::Property;
PropertyLength& operator=(const T& val) {
Property<T>::operator=(val);
return *this;
}
// ミリメートル(mm)に変換
T ToMM() const {
return ToM() * static_cast<T>(1000.0);
}
// センチメートル(cm)に変換
T ToCM() const {
return ToM() * static_cast<T>(100.0);
}
// メートル(標準値)を取得
T ToM() const {
return static_cast<T>(*this);
}
// キロメートル(km)に変換
T ToKM() const {
return ToM() / static_cast<T>(1000.0);
}
};
// ---- クランプされた値を持つプロパティ
template <typename T>
class PropertyClamped : public Property<T> {
public:
// 値を加工して返すだけのラムダ式を渡す!
PropertyClamped(const T& initial_val, const T& min_v, const T& max_v)
: Property<T>(
initial_val,
[min_v, max_v](const T& val) {
return std::clamp(val, min_v, max_v);
}
) {}
PropertyClamped& operator=(const T& val) {
Property<T>::operator=(val);
return *this;
}
};
- = による代入
- +=、-=、*=、/= による四則演算
- ++、-- によるインクリメント・デクリメント
- << による、std::coutなどの出力表示
サンプルコード
config.hpp
#pragma once
#include "Property.hpp"
#include <iostream>
class Config
{
public:
Config() = default;
~Config() = default;
Property<int> Width = Property<int>(
[&](const int& val) {
width_value = val;
std::cout << " [Set] Width set to: " << width_value << std::endl;
return width_value;
}
);
Property<int> Height = { 0 };
Property<std::string> Title = { "Default Title" };
Property<double> Fullscreen = { 0.0 };
PropertyDeg<double> Angle = { 0.0 }; // 角度を扱うプロパティ
PropertyLength<double> Length = { 0.0 }; // 長さを扱うプロパティ
// 0~100 の範囲にクランプされるプロパティの例
// setter で値をクランプするように設定
//Property<int> Int_0to100 = Property<int>(
// [&](const int &v) {
// // *this = ... ではなく、内部変数を直接更新するメソッドを呼ぶ
// Int_0to100.set_value(std::clamp(v, 0, 100));
// }
//);
PropertyClamped<int> Int_0to100 = PropertyClamped<int>(50, 0, 100);
// Read-Only プロパティの例
// getter のみを指定して、setter は nullptr にすることで Read-Only プロパティを作成
Property<int> status = Property<int>(
[&]() {
return status_value;
}
);
void inc_status() {
status_value++;
}
private:
double width_value = 0.0;
int status_value = 100;
};
main.cpp
#include "simple_json.hpp"
#include "config.hpp"
#include <iostream>
# include <iomanip>
int main()
{
// プロパティの使用例
Config config;
config.Width = 800;
config.Width += 800;
config.Height = 600;
config.Fullscreen = 1.23456;
config.Title = "My Application";
config.Angle = 45.0; // 角度を設定
config.Length = 2.5; // 長さを設定
// プロパティの値を出力
std::cout << "Width: " << config.Width << std::endl;
std::cout << "Height: " << config.Height << std::endl;
std::cout << std::endl;
std::cout << std::fixed << std::setprecision(4);
std::cout << "Fullscreen: " << static_cast<double>(config.Fullscreen) << std::endl;
std::cout << "Fullscreen: " << config.Fullscreen << std::endl;
std::cout << std::endl;
std::cout << "Title: " << static_cast<std::string>(config.Title) << std::endl;
std::cout << "Title: " << config.Title << std::endl;
// 角度と長さのプロパティの使用例
std::cout << std::endl;
std::cout << "Angle (deg): " << config.Angle << std::endl;
std::cout << "Angle (deg): " << config.Angle.ToDeg() << std::endl;
std::cout << "Angle (rad): " << config.Angle.ToRad() << std::endl;
// 長さのプロパティの使用例
std::cout << std::endl;
std::cout << "Length (mm): " << config.Length.ToMM() << std::endl;
std::cout << "Length (cm): " << config.Length.ToCM() << std::endl;
std::cout << "Length (m): " << config.Length << std::endl;
std::cout << "Length (m): " << config.Length.ToM() << std::endl;
std::cout << "Length (km): " << config.Length.ToKM() << std::endl;
// 0~100 の範囲にクランプされるプロパティの使用例
std::cout << std::endl;
config.Int_0to100 = 150; // 150 は 100 にクランプされる
std::cout << "Int_0to100: " << config.Int_0to100 << std::endl;
// Read-Only プロパティの使用例
std::cout << std::endl;
std::cout << "Status: " << config.status << std::endl;
config.inc_status(); // status_value をインクリメント
std::cout << "Status (inc): " << config.status << std::endl;
return 0;
}
出力結果
[Set] Width set to: 800 # setter経由して出力
[Set] Width set to: 1600 # setter経由して出力
Width: 1600
Height: 600
Fullscreen: 1.2346
Fullscreen: 1.2346
Title: My Application
Title: My Application
Angle (deg): 45.0000
Angle (deg): 45.0000
Angle (rad): 0.7854
Length (mm): 2500.0000
Length (cm): 250.0000
Length (m): 2.5000
Length (m): 2.5000
Length (km): 0.0025
Int_0to100: 100
Status: 100
Status (inc): 101
まとめ
プロパティの追加が1行で済み、テンプレートによりコードも共通化できたため
開発およびメンテナンス性がかなり良くなったと思う。
Propertyテンプレートだけでは、値の代入・読込みを指定だけなので Publicのメンバ変数と
何ら変わりがない(何ならオーバーヘッドあるだけ悪化)ですが、値の変化を確認するために
デバックログを追加したり、値の上下限チェックなどを追加するメンバ拡張するなど考慮すれば
役に立つはず...