对C++的改造#1 属性(1)

it2022-05-05  176

本人经常无聊就会写一些乱七八糟的东西,C++11标准出来以后里面的很多东西可以帮我们实现很多很有意思的功能,这次就是要实现一个类似C#的属性的功能

本套文章仅适用于VS2013环境:低版本VS对C++11标准支持不完全;g++禁止C风格强制将lambda转换为std::function,不过可以考虑牺牲一部分特性来支持其他编译器,不过不在本文讨论范畴。

注意,该实现方法会多消耗很多资源,不建议用在对效率以及资源要求较高的大型工程中,不过用在小项目中或者干脆是拿来练手还是不错的。

 


分析属性的特征:

C#属性,就像下面这个:

int Property{ get{ ... } set{ ... } }

可以看出,属性分为这几个部分成几个部分:类型,名字,get函数,set函数。

两个函数,我们可以用std::function代替。

名字,类型,普通变量就有这两个东西,不过为了封装成属性,我们肯定要定义一个结构将两个函数封装进去,那么怎么使一个既定的结构拥有不同类型的特性?那就是使用模板类型。

那么一个属性的基本结构就如下所示:

template<class T> class Property { typedef std::function <T()> GetFunc; typedef std::function <void(T)> SetFunc; T _value; GetFunc _getFunc = nullptr; SetFunc _setFunc = nullptr; }

对get,set函数进行封装

因为_getFunc,_setFunc可能为空,我们不能直接调用,为了避免每次判断,并且在为空时提供默认操作,我们需要对着两个函数进行封装。

T get() const { if (!_getFunc) return _value; return _value; } void set(T value) { if (_setFunc) { _setFunc(value); } else { _value = value; } }

 

添加构造函数

我们肯定不愿意用 myProp._getFunc=[](){return value;} 这种方式来对get,set函数进行定义,并且无法在头文件内进行内联定义,这似乎有点不太符合我们的需求,我们希望的是一个像C#那样定义的属性。那么我们可以通过构造函数把两个参数传入,而且,除了两个参数之外,还可以把默认值也一起传入,就像delphi的属性定义default关键字那样,而且还不用像delphi一样把get,set函数与属性本身分开定义。

private: void Init() { InitWithGS((GetFunc)nullptr, (SetFunc)nullptr); } void InitWithG(GetFunc getFunc) { InitWithGS(getFunc, (SetFunc)nullptr); } void InitWithGS(GetFunc getFunc, SetFunc setFunc) { _getFunc = getFunc; _setFunc = setFunc; } void InitWithV(const T& value) { InitWithGSV((GetFunc)nullptr, (SetFunc)nullptr, value); } void InitWithGV(GetFunc getFunc, const T& value) { InitWithGSV(getFunc, (SetFunc)nullptr, value); } void InitWithGSV(GetFunc getFunc, SetFunc setFunc, const T& value) { _getFunc = getFunc; _setFunc = setFunc; set(value); } public: Property() { Init(init); } Property(GetFunc getFunc) { InitWithG(getFunc,init); } Property(GetFunc getFunc, SetFunc setFunc) { InitWithGS(getFunc, setFunc, init); } Property(GetFunc getFunc, const T& value) { InitWithGV(getFunc, value); } Property(const Property<T>& value) { InitWithV(value.get()); } Property(const T& value) { InitWithV(value); } Property(GetFunc getFunc, SetFunc setFunc, const T& value) { InitWithGSV(getFunc, setFunc, value); }

 

我采用了委托构造的形式,这不是必要的,想怎么写按你自己喜好。另外,我没有定义只写模式的构造函数,因为我一直没用到那东西,需要的话可以自己添加定义。

现在,我们可以使用如下方式定义属性: 

//默认 Property<int> x; //读写 Property<float> y{ (Property<float>::GetFunc)[=]()->float{ ... }, (Property<float>::SetFunc)[=](float value){ ... }, 0.0f }; //只读 std::string s; const Property<std::string>{ (Property<std::string>::GetFunc)[=]()->std::string{ return s; } }

 

因为C++11的初始化列表特性,我把构造函数的圆括号换成了大括号,这样就更加接近了C#的阅读风格。

不过我们可以发现,定义匿名函数的前缀以及类型转换运算符太长,不便于记忆,需要简化。所以我们可以定义两个宏,来代替那冗长的前缀。另外默认值我也定义了一个宏,这样看起来会更加规范。

#define GET(type) (Property<type>::GetFunc)[=]()->type #define SET(type) (Property<type>::SetFunc)[=](type value) #define DEFAULT(value) value //默认 Property<int> x; //读写 Property<float> y{ GET(float){ ... }, SET(float){ ... }, DEFAULT(0.0f) }; //只读 std::string s; const Property<std::string>{ GET(float){ return s; } };

 

这样定义一个属性的方式就已经很接近C#了,不过还是有点小区别:

1.类型必须用Property<>包裹,否则无法跟变量区分开。这是没办法的事,毕竟是不同的语言。

2.定义默认值是必要的,因为C++的变量初始值完全无法预判。

3.GET,SET以及默认值之间有逗号分隔,定义完后有分号。这也是语言区别造成的,初始化列表毕竟是另外一个东西。

4.GET,SET必须要代入类型,这个目前我没有想到解决办法,C++11标准匿名函数不支持自动推断参数类型,现在的编译器也无法根据代入的匿名函数自动推断是哪个构造函数而采用相应的方法构造。也许等编译器发展后有可能吧。(其实我感觉我这么压榨编译器是不是有点不厚道,话说后面讲反射的实现的时候那更压榨编译器)

添加操作

现在我们只是简单封装了一个类似属性的数据结构,但它根本没法拿来用,因为它是T类型完全不同的类型。要怎么才能用呢?其实我们分析一下就能发现,只要重载所有运算符就行了。

先说最重要的两个:等号左值以及等号右值

等号左值有两种:构造用和赋值用。构造用说白了就是能用一个T类型的值隐式构造一个Property<T>类型的实例,那么定义一个构造函数就行了,然后我们发现,在上面定义构造函数的时候已经定义了,所以这只用再定义赋值用的即可,这里需要注意一点,如果使用同类型Property相互赋值,那么只应该传递值而不应该传递函数,所以也需要重载。

T operator=(const T& value){ set(value); return value; } T operator=(const Property<T>& value){ set(value._value); return value; }

好了,现在等号左值已近重载完成,该重载等号右值了,这里说一个概念,所谓的重载等号右值其实就是重载强制类型转换运算符,我们需要把它转换为T类型,那么就重载T类型的强制转换运算符。

operator T() const{ return get(); }

 

好了,如果你足很懒的话,现在这样就已经够了,已经能满足一个属性的基本需求了,不过很多时候跟其他代码的接触还是不够平滑,很多时候还是必须调用get和set函数,下一篇文章应该会讲怎么让它更平滑。(其实就是把其他能重载的操作符都给重载了)

转载于:https://www.cnblogs.com/zwork/p/3863246.html


最新回复(0)