概要
とファントムタイプの投稿を組み合わせていい感じにしました。
宣言は一行かつ、なるべく普通の変数と同じようなアクセスができることを目指しました。
先に使い方をのせます。
使い方
#include <vector>
struct Hoge : public PropertyEnabled<Hoge> {
public:
// ≒ public int Num { get; private set; }
PublicGetPrivateSet<int> Num;
// ≒ public List<int> Ints { get; private set; }
PublicGetPrivateSet<std::vector<int>> Ints{ 1,3 };
// ≒ private List<int> intsC ;
// public ReadOnlyCollection<int> IntsC => intsC.AsReadOnly();
ReadonlyPublicGetPrivateSet<std::vector<int>> IntsR{ 3,4,5 };
//クラス内からのアクセス
void Fuga() {
//Num
int num = Num; //ok 暗黙の変換
Num = 4; //ok 代入
//Ints
std::vector<int> ints = Ints; //ok 暗黙の変換
Ints.push_back(3); //ok 非constメンバアクセス
Ints[0] = 2; //ok 非constインデックスアクセス
Ints = ints; //ok 代入
//IntsR
std::vector<int> intsC = IntsR; //ok 暗黙の変換
IntsR.push_back(2); //ok 非constメンバアクセス
IntsR[0] = 2; //ok 非constインデックスアクセス
IntsR = intsC; //ok 代入
}
};
//クラス外からのアクセス
int main() {
Hoge h;
//Num
int num = h.Num; //ok 暗黙の変換
h.Num = 4; //error 代入
//Ints
std::vector<int> ints = h.Ints; //ok 暗黙の変換
h.Ints.push_back(3); //ok 非constメンバアクセス
h.Ints[0] = 2; //ok 非constインデックスアクセス
h.Ints = ints; //error 代入
//IntsR
std::vector<int> intsC = h.IntsR; //error 暗黙の変換 (private継承してるので関数を通さないとerror)
intsC = h.IntsR.Get(); //ok アクセス(関数経由)
h.IntsR->size(); //ok constメンバアクセス(演算子経由)
auto s = h.IntsR.At(0); //ok constインデックスアクセス(関数経由)
h.IntsR = intsC; //error 代入
h.IntsR->push_back(); //error 非constメンバアクセス(演算子経由してもダメ)
h.IntsR.At(0) = 2; //error 非constインデックスアクセス(関数経由してもダメ)
}
コメントを見ればだいたい分かると思うのでReadonlyPublicGetPrivateSet
だけ説明します。(長いけど他に思いつかなかった)
PublicGetPrivateSet
で宣言するとそのクラスの外からの代入はエラーにできますが、メンバ変数の書き換えや非constメンバ関数の呼び出しは防げません。
ReadonlyPublicGetPrivateSet
はこれを防ぐためにprivate継承を使って外からはconstアクセスだけに制限したものです。
環境
C++20
Visual Studio 2019 16.11.8
実装
・CRTPのためにネストさせる。
・型引数が基本型(intやfloatなど)かどうかで分ける部分をFundamentalBase
とNotFundamentalBase
に実装。
・同じ名前で使えるように継承とクラステンプレートの部分特殊化を使ってPublicGetPrivateSet
にまとめる。
・基本型じゃない場合ReadonlyにできるようにNotFundamentalBase
をprivate継承してReadonlyPublicGetPrivateSet
を実装。
という風にしました。最終的にこんな感じです。
template<class Owner>
class PropertyEnabled {
private:
/// <summary>
/// _Innerが基本型なのでメンバ変数にもつ
/// </summary>
template<class _Inner>
requires std::is_fundamental_v<_Inner>
class FundamentalBase {
//...
};
/// <summary>
/// _Innerが基本型じゃないので継承する
///</summary>
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class NotFundamentalBase :public _Inner {
//...
};
public:
//部分特殊化を使って同じ名前でどんな型でも使えるようにする
template<class _Inner>
class PublicGetPrivateSet;
template<class _Inner>
requires std::is_fundamental_v<_Inner>
class PublicGetPrivateSet<_Inner> :public FundamentalBase<_Inner> {
//...
};
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class PublicGetPrivateSet<_Inner> :public NotFundamentalBase<_Inner> {
//...
};
/// <summary>
/// クラス外からのconstアクセス関数を実装
/// </summary>
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class ReadonlyPublicGetPrivateSet :private NotFundamentalBase<_Inner> {
//...
};
};
基本型を型引数にとる場合のベースクラス
ほとんど先駆者のものと同じです。
それにくわえて代入にかかわる演算子も沢山オーバーロードして使いやすくしました。
/// <summary>
/// _Innerが基本型なのでメンバ変数にもつ
/// 基本型なので値渡し
/// </summary>
template<class _Inner>
requires std::is_fundamental_v<_Inner>
class FundamentalBase {
public:
using Owner = _Owner;
using Outer = PropertyEnabled<Owner>;
using Inner = _Inner;
using This = FundamentalBase<Inner>;
private:
Inner inner;
protected:
constexpr FundamentalBase()noexcept : inner() {
}
constexpr FundamentalBase(Inner t)noexcept : inner(t) {
}
public:
constexpr virtual ~FundamentalBase()noexcept {
}
//getter
constexpr operator Inner () const {
return inner;
}
protected:
constexpr Inner& GetRef() {
return inner;
}
//setter
This& operator=(Inner r) {
this->inner = r;
return *this;
}
//setter
This& operator=(This r) {
this->inner = r.inner;
return *this;
}
//あとは利便性のために演算子オーバーロード
This& operator++() {
++this->inner;
return *this;
}
This& operator--() {
--this->inner;
return *this;
}
This operator++(int) {
auto temp = *this;
++this->inner;
return temp;
}
This operator--(int) {
auto temp = *this;
--this->inner;
return temp;
}
This& operator+=(Inner r) {
this->inner += r;
return *this;
}
This& operator-=(Inner r) {
this->inner -= r;
return *this;
}
This& operator*=(Inner r) {
this->inner *= r;
return *this;
}
This& operator/=(Inner r) {
this->inner /= r;
return *this;
}
This& operator%=(int r) {
this->inner %= r;
return *this;
}
This& operator<<=(int r) {
this->inner <<= r;
return *this;
}
This& operator>>=(int r) {
this->inner >>= r;
return *this;
}
};
operator<=>?
三方比較演算子と言って比較演算子を自動定義してくれます。
暗黙的キャストするのでいらなかったため削除。
基本型じゃない型を型引数にとる場合のベースクラス
型引数を継承して上手いことやります。
/// <summary>
/// _Innerが基本型じゃないので継承する
/// ※virtualデストラクタがないクラスでアップキャストしてdeleteすると未定義
///</summary>
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class NotFundamentalBase :public _Inner {
public:
using Owner = _Owner;
using Outer = PropertyEnabled<Owner>;
using Inner = _Inner;
using This = NotFundamentalBase<Inner>;
protected:
using Inner::Inner;
//メンバイニシャライザのために追加
template <class... Args>
requires std::constructible_from<Inner, Args...>
constexpr NotFundamentalBase(Args&&... args) :Inner(std::forward<Args>(args)...) {
}
public:
constexpr virtual ~NotFundamentalBase()noexcept {
}
protected:
constexpr Inner& GetRef() {
return *this;
}
//setter
This& operator=(const Inner& r) {
((Inner)*this) = r;
return *this;
}
//setter
This& operator=(const This& r) {
((Inner)*this) = r;
return *this;
}
};
コンストラクタについて
ファントムタイプの投稿にあるように、{ }
でコンストラクタを呼んだ場合initializer_list
になるのか {第一引数,第二引数...}
となるのか分かりにくいことがあります。参考にした投稿では静的メソッドで生成していましたが、initializer_list
を受け取ったものの構築できないときにstatic_assert
をだすことで対応しました。
よく考えたらusing
でコンストラクタを継承するだけで上手くいったので修正しました。
参考にした投稿ではexplicit
を付けるために複雑になってたみたいです。
ただ継承したコンストラクタだけだとメンバイニシャライザで呼ぶ際エラーになったので1つだけ残しています。
何でもかんでも継承することについて
virtualデストラクタがないクラスも継承してしまうため、そのクラスにアップキャストしてdeleteすると未定義動作になります。具体的にはstd::vectorなどで、
//子クラスでnewして暗黙的にアップキャスト
std::vector<int>* v= new NotFundamentalBase<std::vector<int>>();
delete v;//親クラスのポインタをdelete <-未定義動作 どうなるか分からない
みたいなかんじでポリモーフィズムとかに使うと危ないみたいですが、単に自動実装プロパティとして使うなら問題ないはずです。
同じ名前でどんな型でも非Readonlyで使えるようにする
public継承してfriendするだけですが、親のコンストラクタと代入演算子は自動的に継承されないので個別にusingします。
もっと簡潔に書く方法あるのかも・・・
//部分特殊化を使って同じ名前でどんな型でも使えるようにする
template<class _Inner>
class PublicGetPrivateSet;
template<class _Inner>
requires std::is_fundamental_v<_Inner>
class PublicGetPrivateSet<_Inner> :public FundamentalBase<_Inner> {
public:
using Inner = _Inner;
using Base = FundamentalBase<Inner>;
using This = PublicGetPrivateSet<Inner>;
friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
protected:
using Base::FundamentalBase;
using Base::operator=;
PublicGetPrivateSet() = default;
This& operator=(This&) = default;
This& operator=(This&&) = default;
};
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class PublicGetPrivateSet<_Inner> :public NotFundamentalBase<_Inner> {
public:
using Inner = _Inner;
using Base = NotFundamentalBase<Inner>;
using This = PublicGetPrivateSet<Inner>;
friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
protected:
using Base::NotFundamentalBase;
using Base::operator=;
PublicGetPrivateSet() = default;
This& operator=(This&) = default;
This& operator=(This&&) = default;
};
基本型じゃない型を型引数にとってReadonlyにするクラス
まず、private継承を使うことでプロパティを宣言したクラスの外からアクセスできなくします。ただ、constアクセスだけは許可したいので専用の演算子や関数を実装します。
このときoperator[]
を使ってしまうとクラス内からも非constアクセスができなくなってしまうので注意。また、operator*
やoperator()
にしてもいいですが可読性を考えて関数にしています。
/// <summary>
/// NotFundamentalBaseからprivate継承
/// クラス外からのconstアクセス関数を実装
/// </summary>
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class ReadonlyPublicGetPrivateSet :private NotFundamentalBase<_Inner> {
public:
using Owner = _Owner;
using Outer = PropertyEnabled<Owner>;
using Inner = _Inner;
using Base = NotFundamentalBase<Inner>;
using This = ReadonlyPublicGetPrivateSet<Inner>;
friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
protected:
using Base::NotFundamentalBase;
using Base::operator=;
ReadonlyPublicGetPrivateSet() = default;
This& operator=(This&) = default;
This& operator=(This&&) = default;
public:
//本体にアクセス
const Inner& Get()const noexcept {
return *this;
}
//メンバアクセス
const Inner* operator->()const noexcept {
return this;
}
//インデックスアクセス
template<typename U>
auto At(const U& idx)const {
return ((const Inner&)*this)[idx];
}
};
追記 ProtectedSet版
欲しくなったので追加。
PrivateSet版を継承するのは設計的にどうかと思ったのでコピペしてfriendを追加しました。
子クラスからはPropertyEnabled<Owner>::Access<T>
を経由して非constアクセスします。
追加にあたってFundamentalBase
とNotFundamentalBase
クラスの関数を増やし、下のコード全文も更新しました。
template<class Owner>
class PropertyEnabled {
private:
///ベースクラス
public:
///ここにPrivateSet版
//ProtectedSet版
//ProtectedSet版
template<class _Inner>
class PublicGetProtectedSet;
template<class _Inner>
requires std::is_fundamental_v<_Inner>
class PublicGetProtectedSet<_Inner> :public FundamentalBase<_Inner> {
public:
using Inner = _Inner;
using Base = FundamentalBase<Inner>;
using This = PublicGetProtectedSet<Inner>;
friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
friend PropertyEnabled<Owner>;
protected:
using Base::FundamentalBase;
using Base::operator=;
PublicGetProtectedSet() = default;
This& operator=(This&) = default;
This& operator=(This&&) = default;
};
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class PublicGetProtectedSet<_Inner> :public NotFundamentalBase<_Inner> {
public:
using Inner = _Inner;
using Base = NotFundamentalBase<Inner>;
using This = PublicGetProtectedSet<Inner>;
friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
friend PropertyEnabled<Owner>;
protected:
using Base::NotFundamentalBase;
using Base::operator=;
PublicGetProtectedSet() = default;
This& operator=(This&) = default;
This& operator=(This&&) = default;
};
/// <summary>
/// NotFundamentalBaseからprivate継承
/// クラス外からのconstアクセス関数を実装
/// </summary>
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class ReadonlyPublicGetProtectedSet :private NotFundamentalBase<_Inner> {
public:
using Owner = _Owner;
using Outer = PropertyEnabled<Owner>;
using Inner = _Inner;
using Base = NotFundamentalBase<Inner>;
using This = ReadonlyPublicGetProtectedSet<Inner>;
friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
friend PropertyEnabled<Owner>;
protected:
using Base::NotFundamentalBase;
using Base::operator=;
ReadonlyPublicGetProtectedSet() = default;
This& operator=(This&) = default;
This& operator=(This&&) = default;
public:
//本体にアクセス
const Inner& Get()const noexcept {
return *this;
}
//メンバアクセス
const Inner* operator->()const noexcept {
return this;
}
//インデックスアクセス
template<typename U>
auto At(const U& idx)const {
return ((const Inner&)*this)[idx];
}
};
protected:
//子クラスからProtectedSet版へのアクセス
template <class T>
requires std::same_as< Owner, typename T::Owner>
auto Access(T& target)->T::Inner& {
return target.GetRef();
}
};
コード全文
長いもののわりと読みやすく書けたと思うのですがどうでしょう?
長いので省略
#pragma once
#include <concepts>
#include <initializer_list>
#include <utility>
template<class _Owner>
class PropertyEnabled {
public:
using Owner = _Owner;
using Outer = PropertyEnabled<Owner>;
private:
/// <summary>
/// _Innerが基本型なのでメンバ変数にもつ
/// 基本型なので値渡し
/// </summary>
template<class _Inner>
requires std::is_fundamental_v<_Inner>
class FundamentalBase {
public:
using Owner = _Owner;
using Outer = PropertyEnabled<Owner>;
using Inner = _Inner;
using This = FundamentalBase<Inner>;
private:
Inner inner;
protected:
constexpr FundamentalBase()noexcept : inner() {
}
constexpr FundamentalBase(Inner t)noexcept : inner(t) {
}
public:
constexpr virtual ~FundamentalBase()noexcept {
}
//getter
constexpr operator Inner () const {
return inner;
}
protected:
constexpr Inner& GetRef() {
return inner;
}
//setter
This& operator=(Inner r) {
this->inner = r;
return *this;
}
//setter
This& operator=(This r) {
this->inner = r.inner;
return *this;
}
//あとは利便性のために演算子オーバーロード
This& operator++() {
++this->inner;
return *this;
}
This& operator--() {
--this->inner;
return *this;
}
This operator++(int) {
auto temp = *this;
++this->inner;
return temp;
}
This operator--(int) {
auto temp = *this;
--this->inner;
return temp;
}
This& operator+=(Inner r) {
this->inner += r;
return *this;
}
This& operator-=(Inner r) {
this->inner -= r;
return *this;
}
This& operator*=(Inner r) {
this->inner *= r;
return *this;
}
This& operator/=(Inner r) {
this->inner /= r;
return *this;
}
This& operator%=(int r) {
this->inner %= r;
return *this;
}
This& operator<<=(int r) {
this->inner <<= r;
return *this;
}
This& operator>>=(int r) {
this->inner >>= r;
return *this;
}
};
/// <summary>
/// _Innerが基本型じゃないので継承する
/// ※virtualデストラクタがないクラスでアップキャストしてdeleteすると未定義
///</summary>
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class NotFundamentalBase :public _Inner {
public:
using Owner = _Owner;
using Outer = PropertyEnabled<Owner>;
using Inner = _Inner;
using This = NotFundamentalBase<Inner>;
protected:
using Inner::Inner;
//メンバイニシャライザのために追加
template <class... Args>
requires std::constructible_from<Inner, Args...>
constexpr NotFundamentalBase(Args&&... args) :Inner(std::forward<Args>(args)...) {
}
public:
constexpr virtual ~NotFundamentalBase()noexcept {
}
protected:
constexpr Inner& GetRef() {
return *this;
}
//setter
This& operator=(const Inner& r) {
((Inner)*this) = r;
return *this;
}
//setter
This& operator=(const This& r) {
((Inner)*this) = r;
return *this;
}
};
public:
//部分特殊化を使って同じ名前でどんな型でも使えるようにする
template<class _Inner>
class PublicGetPrivateSet;
template<class _Inner>
requires std::is_fundamental_v<_Inner>
class PublicGetPrivateSet<_Inner> :public FundamentalBase<_Inner> {
public:
using Inner = _Inner;
using Base = FundamentalBase<Inner>;
using This = PublicGetPrivateSet<Inner>;
friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
protected:
using Base::FundamentalBase;
using Base::operator=;
PublicGetPrivateSet() = default;
This& operator=(This&) = default;
This& operator=(This&&) = default;
};
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class PublicGetPrivateSet<_Inner> :public NotFundamentalBase<_Inner> {
public:
using Inner = _Inner;
using Base = NotFundamentalBase<Inner>;
using This = PublicGetPrivateSet<Inner>;
friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
protected:
using Base::NotFundamentalBase;
using Base::operator=;
PublicGetPrivateSet() = default;
This& operator=(This&) = default;
This& operator=(This&&) = default;
};
/// <summary>
/// NotFundamentalBaseからprivate継承
/// クラス外からのconstアクセス関数を実装
/// </summary>
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class ReadonlyPublicGetPrivateSet :private NotFundamentalBase<_Inner> {
public:
using Owner = _Owner;
using Outer = PropertyEnabled<Owner>;
using Inner = _Inner;
using Base = NotFundamentalBase<Inner>;
using This = ReadonlyPublicGetPrivateSet<Inner>;
friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
protected:
using Base::NotFundamentalBase;
using Base::operator=;
ReadonlyPublicGetPrivateSet() = default;
This& operator=(This&) = default;
This& operator=(This&&) = default;
public:
//本体にアクセス
const Inner& Get()const noexcept {
return *this;
}
//メンバアクセス
const Inner* operator->()const noexcept {
return this;
}
//インデックスアクセス
template<typename U>
auto At(const U& idx)const {
return ((const Inner&)*this)[idx];
}
};
//ProtectedSet版
template<class _Inner>
class PublicGetProtectedSet;
template<class _Inner>
requires std::is_fundamental_v<_Inner>
class PublicGetProtectedSet<_Inner> :public FundamentalBase<_Inner> {
public:
using Inner = _Inner;
using Base = FundamentalBase<Inner>;
using This = PublicGetProtectedSet<Inner>;
friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
friend PropertyEnabled<Owner>;
protected:
using Base::FundamentalBase;
using Base::operator=;
PublicGetProtectedSet() = default;
This& operator=(This&) = default;
This& operator=(This&&) = default;
};
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class PublicGetProtectedSet<_Inner> :public NotFundamentalBase<_Inner> {
public:
using Inner = _Inner;
using Base = NotFundamentalBase<Inner>;
using This = PublicGetProtectedSet<Inner>;
friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
friend PropertyEnabled<Owner>;
protected:
using Base::NotFundamentalBase;
using Base::operator=;
PublicGetProtectedSet() = default;
This& operator=(This&) = default;
This& operator=(This&&) = default;
};
/// <summary>
/// NotFundamentalBaseからprivate継承
/// クラス外からのconstアクセス関数を実装
/// </summary>
template <class _Inner>
requires (!std::is_fundamental_v<_Inner>)
class ReadonlyPublicGetProtectedSet :private NotFundamentalBase<_Inner> {
public:
using Owner = _Owner;
using Outer = PropertyEnabled<Owner>;
using Inner = _Inner;
using Base = NotFundamentalBase<Inner>;
using This = ReadonlyPublicGetProtectedSet<Inner>;
friend typename std::enable_if<std::is_class<Owner>::value, Owner>::type;
friend PropertyEnabled<Owner>;
protected:
using Base::NotFundamentalBase;
using Base::operator=;
ReadonlyPublicGetProtectedSet() = default;
This& operator=(This&) = default;
This& operator=(This&&) = default;
public:
//本体にアクセス
const Inner& Get()const noexcept {
return *this;
}
//メンバアクセス
const Inner* operator->()const noexcept {
return this;
}
//インデックスアクセス
template<typename U>
auto At(const U& idx)const {
return ((const Inner&)*this)[idx];
}
};
protected:
//子クラスからProtectedSet版へのアクセス
template <class T>
requires std::same_as< Owner, typename T::Owner>
auto Access(T& target)->T::Inner& {
return target.GetRef();
}
};
終わりに
マクロに逃げたり宣言2行で妥協したりconst偽装を試したりしましたが、最終的にわりといいかんじになったんじゃないかと思います。
visual studio2019だとrequires文の次の行が勝手にインデントされるのなんとかならないものか・・・