C++ 实现 C# 属性和索引器,带访问器修饰符






4.92/5 (23投票s)
C++ 实现 C# 的属性和索引器功能,并通过访问器修饰符控制其编译时可访问性。
引言
本文介绍在 C++ 中实现 C# 的**属性**和**索引器**功能,并使用访问器修饰符控制它们的编译时可访问性。
提供的源代码包含了一个通用的 C++ 属性和索引器实现框架。用户只需实现一个**访问器函子**,该函子需要包含 `set` 和 `get` 访问器函数。框架提供了预定义的属性,每种属性都具有不同的可访问性选项,它们将包装用户定义的访问器函子。因此,在宿主类中声明一个属性,只需根据预期的可访问性选择一个属性,并将其分配给一个访问器即可。
**属性**被称为智能字段,它们允许访问类的成员变量,同时通过隐式的 `set` 和 `get` 访问器来维护封装。
**索引器**在 C# 中也称为智能数组,可以用来将对象当作数组使用。与 C# 属性类似,**下标 ( `[]` ) 运算符**可以访问其自己的 `set` 和 `get` 访问器。
Doxygen 生成的源代码在此处可用:文档。
背景
我一直使用 C++ 作为我的编程语言,它也是我学习 Java 和 C# 等其他编程语言的基础。每当我在其他语言中看到某个特性时,我都会好奇它是否能在 C++ 中实现。C# 的属性和索引器就是这样一个例子。
在 CodeProject 上,我阅读过几篇关于 C++ 实现 C# 属性的文章,但没有一篇涉及到 C# 索引器。然而,我也没有找到一篇提供对属性字段已声明可访问性的编译时检查的文章。
本文提供的代码在 C++ 中实现了属性和索引器的功能,包括对为 `set` 和 `get` 访问器分配的访问器修饰符的可访问性进行编译时检查。
C++ 属性布局
协作模型
在此 C++ 实现的 C# 属性中,有三个组件:
- **访问器** 定义了 `set` 或 `get` 访问器成员函数如何管理值。值可以驻留在访问器的作用域内,也可以驻留在其 `friend` 宿主的 scope 内。
- **属性** 是单个**访问器**引用的容器,它通过其访问函数(即赋值和转换运算符)定义对访问器 `set` 和 `get` 成员函数的访问范围(`public` 或 `private`)。
- **宿主** 是一个可能引用一个或多个**属性**的容器。宿主是属性和访问器的 `friend`。这样做的目的是,如果属性的访问函数(即赋值和转换运算符)对宿主类作用域之外的实体声明为 `private`,宿主仍然可以直接通过其引用的属性,或间接通过属性引用的访问器来访问其内部作用域内的值。
调用模型
此处列出了此 C++ 实现的调用模型,从赋值到通过 C# 属性获取值。
- 对**属性**字段的值赋值是通过其赋值运算符完成的。
- 如果此运算符的访问修饰符是 `public`,那么宿主类作用域之外的任何实体都可以进行属性的值赋值。
- 如果此运算符的访问修饰符是 `private`,那么只有宿主可以进行属性的值赋值。
- 所有属性值赋值都将传递给访问器的 `public` `set` 成员函数。
- `set` 访问器将值传递给任何已分配的资源。已分配的资源可以位于**访问器**的作用域内,也可以位于其 `friend` **宿主**的作用域内。
- 从属性字段检索值是通过转换运算符完成的。
- 如果此运算符的访问修饰符是 `public`,那么宿主类作用域之外的任何实体都可以请求属性的值。
- 如果此运算符的访问修饰符是 `private`,那么只有宿主可以请求属性的值。
- 所有属性值请求都将传递给**访问器**的 `public` `get` 成员函数。
- `get` 访问器从任何已分配的资源请求值。与 `set` 访问器一样,已分配的资源可以位于访问器作用域内,也可以位于其 `friend` 宿主作用域内。
访问器接口和基类
每个属性的访问器都必须定义以下函数:
- **`get`** 属性访问器用于返回属性值。
- **`set`** 属性访问器用于分配新值。
接口 **`IAccessor`** 定义了预期的访问器函数,抽象基类 **`AccessorBase`** 保存了访问器函数共享的 `ValueType` 值。
template <class Host, typename ValueType>
class IAccessor
{
public:
virtual void set(Host*, const ValueType) = 0;
virtual ValueType get(Host*) const = 0;
};
template <class Host, typename ValueType>
class AccessorBase : public IAccessor<Host, ValueType>
{
public:
typedef ValueType value_type;
};
访问器实现示例
类 **`Number_Accessor`** 是 `AccessorBase` 类的派生类,其 `ValueType` 为 `int`。在此示例实现中,`set` 和 `get` 访问器的值引用位于该类本身的作用域内:**`private int m_value`**。
template <class Host>
class Number_Accessor : public AccessorBase<Host, int>
{
friend Host;
public:
void set(Host * /* _pHost */, const int _value)
{ m_value = _value; }
int get(Host * /* _pHost */) const
{ return m_value; }
private:
int m_value;
};
属性接口和基类
抽象类 **`PropertyBase`** 保存了访问器容器的一个实例。`PropertyBase` 是抽象的,因为它不包含其派生接口 **`IProperty`** 的实现。
template<typename ValueType>
class IProperty
{
virtual ValueType operator =(const ValueType& value) = 0;
virtual operator ValueType() const = 0;
};
template<class Host, class Accessor, typename ValueType>
class PropertyBase : public IProperty<ValueType>
{
public:
PropertyBase(Host *_pHost) : m_pHost( _pHost )
{}
ValueType operator =(const ValueType& _value)
{
m_Accessor.set(m_pHost, _value);
return _value;
}
operator ValueType() const
{ return m_Accessor.get(m_pHost); }
protected:
Host* m_pHost;
Accessor m_Accessor;
};
具有不同访问修饰符的属性
有三种不同的属性类,每种属性类对其 setter 和 getter 函数具有不同的访问修饰符排列:
- **`Property`**:setter 和 getter 功能均为 `public`。
- **`Property_Set_Public`**:setter 为 `public`,getter 为 `private`。
- **`Property_Get_Public`**:setter 为 `private`,getter 为 `public`。
请注意,`Property` 类的访问器方法的访问修饰符均为 `public`。
template<class Host, class Accessor, typename ValueType>
class Property : public PropertyBase<Host, Accessor, ValueType>
{
public:
Property(Host *_pHost = NULL) : PropertyBase( _pHost ){}
public:
using PropertyBase<Host, Accessor, ValueType>::operator =;
public:
using PropertyBase<Host, Accessor, ValueType>::operator ValueType;
};
宿主使用 C++ 属性实现
请理解,本文的目的是提供一个通用的工具包(框架),用于在 C++ 类中实现属性,用户只需要实现一个访问器。
定义访问器
要定义一个访问器,请从 `AccessorBase` 派生一个类,并实现预期的 `set` 和 `get` 访问器函数。
此处,一个名为 `Number_Accessor` 的访问器从 `TAccessor` 派生而来,它封装了 `int` 类型访问器值的范围。
template <class Host, typename ValueType>
struct TAccessor : public AccessorBase<Host, ValueType>
{
void set(Host * /* _pHost */, const ValueType _value)
{ m_value = _value; }
ValueType get(Host * /* _pHost */) const
{ return m_value; }
protected:
ValueType m_value;
};
template <class Host>
struct Number_Accessor : public TAccessor<Host, int> {};
为宿主定义属性
在宿主类中,每个属性都使用其预期的可访问性和访问器进行定义(`typedef`)。
要在此框架中定义一个属性,请根据可访问性需求选择以下属性类之一:`Property`、`Property_Set_Public` 或 `Property_Get_Public`,并将其分配给它将代表的预期访问器。
在接下来的代码示例中,`Test` 类定义了三个具有不同可访问范围的属性,它们都使用了相同的访问器 `Number_Accessor`。
class Test
{
// outside the scope of class Test,
// set and get are both public
typedef ::Property<
::Test,
::Number_Accessor<::Test>,
::Number_Accessor<::Test>::value_type> Property_Number;
// outside the scope of class Test,
// set is public and get is private
typedef ::Property_Set_Public<
::Test,
::Number_Accessor<::Test>,
::Number_Accessor<::Test>::value_type> Property_Set_Public_Number;
// outside the scope of class Test,
// set is private and get is public
typedef ::Property_Get_Public<
::Test,
::Number_Accessor<::Test>,
::Number_Accessor<::Test>::value_type> Property_Get_Public_Number;
public:
Property_Number Number_A;
Property_Set_Public_Number Number_B;
Property_Get_Public_Number Number_C;
};
使用带有属性的宿主
在实际使用刚刚实现的 `Test` 类时,在编译时,您可以看到每个属性(`Number_A`、`Number_B` 和 `Number_C`)的 setter 和 getter 对于其宿主类作用域之外是可访问的。参考以下代码,注意在每个限制性访问的属性处发生的编译时错误。
void main()
{
::Test test;
int i = 0;
// Number_A: Property assignment has set and get accessors as public.
{
test.Number_A = 10;
i = test.Number_A;
}
// Number_B: Property assignment has set accessor as public and get accessor as private.
{
test.Number_B = 10;
// Error C2248: 'Property_Set_Public<Host,Accessor,ValueType>::operator .H' :
// cannot access private member declared in
// class 'Property_Set_Public<Host,Accessor,ValueType>'
// i = test.Number_B;
}
// Number_C: Property assignment has set accessor as private and get accessor as public.
{
// Error C2248: 'Property_Get_Public<Host,Accessor,ValueType>::operator =' :
// cannot access private member declared in
// class 'Property_Get_Public<Host,Accessor,ValueType>'
// test.Number_C = 10;
i = test.Number_C;
}
}
C++ 索引器布局
协作模型
在此 C++ 实现的 C# 索引器中,有三个组件用于通过宿主的*下标运算符*提供访问器功能。
- **`IndexerAccessor`** 与属性的访问器相同,只是 `set` 和 `get` 访问器会接收通过调用的下标运算符提供的索引值。
- **`Indexer`** 与属性相同。
- **宿主**最多可以定义一个下标运算符。当使用宿主的下标运算符时,它会导出一个派生的索引器实例。根据下标运算符的使用方式,赋值操作由索引器的赋值运算符完成,值获取操作由索引器的转换运算符完成。
调用模型
C++ 实现 C# 索引器的调用模型与 C# 属性基本相同,只是宿主的下标运算符的使用是通过派生的索引器实例间接完成的。这里发生的是索引器被用作一个*函子*,即任何可以像函数一样被调用的对象,提供对访问器的访问。
索引器访问器接口和基类
接口 **`IIndexerAccessor`** 与接口 `IAccessor` 的唯一区别是,`set` 和 `get` 访问器成员函数需要一个额外的参数(索引),该参数在使用宿主的下标运算符时提供。
template <class Host, typename ValueType>
class IIndexerAccessor
{
public:
virtual void set(Host*, const int _index, const ValueType) = 0;
virtual ValueType get(Host*, const int _index) const = 0;
};
template <class Host, typename ValueType>
class IndexerAccessorBase : public IIndexerAccessor<Host, ValueType>
{
public:
typedef ValueType value_type;
};
索引器访问器实现示例
类 **`Number_Indexer_Accessor`** 是 `IndexerAccessorBase` 类的派生类,其 `ValueType` 为 `int`。在此示例实现中,`set` 和 `get` 访问器的值引用不在该类本身的作用域内,而是在其宿主的作用域内:**`private int *m_ptrArray`**。
template <class Host>
class Numbers_Indexer_Accessor : public IndexerAccessorBase<Host, int>
{
friend Host;
public:
void set(Host* _pHost, const int _index, const int _value)
{ _pHost->m_ptrArray[_index] = _value; }
int get(Host* _pHost, const int _index) const
{ return _pHost->m_ptrArray[_index]; }
};
索引器接口和基类
抽象类 **`IndexerBase`** 保存了 `IndexerAccessor` 容器的一个实例。`IndexerBase` 是抽象的,因为它不包含其派生接口 **`IIndexer`** 的实现。
具有不同访问修饰符的索引器
与属性一样,有三种不同的索引器,每种索引器对其 setter 和 getter 函数具有不同的访问修饰符排列:
- **`Inderer`**:setter 和 getter 功能均为 `public`。
- **`Indexer_Set_Public`**:setter 为 `public`,getter 为 `private`。
- **`Indexer_Get_Public`**:setter 为 `private`,getter 为 `public`。
请注意,`Indexer` 类的访问器方法的访问修饰符均为 `public`。
template<class Host, class IndexAccessor, typename ValueType>
class Indexer : public IndexerBase<Host,IndexAccessor,ValueType>
{
public:
Indexer(Host *_pHost, int _index = 0) : IndexerBase( _pHost, _index)
{}
public:
using IndexerBase<Host,IndexAccessor,ValueType>::operator =;
public:
using IndexerBase<Host,IndexAccessor,ValueType>::operator ValueType;
};
宿主使用 C++ 索引器实现
这是一个使用 `Indexer` 类和先前实现的名为 `Numbers_Indexer_Accessor` 的 `IndexAccessor` 的简单示例。
宿主的下标运算符和索引器
类 **`Host_Numbers_Indexer`** 使用一个名为 **`Number_Indexer`** 的 `Numbers_Indexer_Accessor` 来定义一个索引器。每次调用宿主的下标运算符(用于赋值或获取值)时,都会提供一个 `Numbers_Indexer` 实例,并作为 setter 和 getter 操作的*函子*。
最重要的是,请特别注意 `Host_Number_Indexer` 的下标运算符返回的内容;它返回一个派生自 `Indexer` 的 `Numbers_Indexer`。
class Host_Numbers_Indexer
{
friend ::Numbers_Indexer_Accessor<::Host_Numbers_Indexer>;
friend ostream &operator<<( ostream &, const Host_Numbers_Indexer & );
typedef ::Indexer <
::Host_Numbers_Indexer,
::Numbers_Indexer_Accessor<::Host_Numbers_Indexer>,
::Numbers_Indexer_Accessor<::Host_Numbers_Indexer>::value_type >
Numbers_Indexer ;
public:
Host_Numbers_Indexer(int _sizeArray = 10)
{
m_sizeArray = ( _sizeArray > 0 ? _sizeArray : 10 );
m_ptrArray = new int[ m_sizeArray ]; // create space for array
assert( m_ptrArray != 0 ); // terminate if memory not allocated
for ( int i = 0; i < static_cast<int>(m_sizeArray); i++ ) {
m_ptrArray[ i ] = i ;
}
}
// subscript operator
Host_Numbers_Indexer::Numbers_Indexer operator[]( int _index )
{ return Numbers_Indexer(this, _index); }
private:
int *m_ptrArray; // pointer to first element of array
size_t m_sizeArray; // m_sizeArray of the array
};
通过下标运算符使用宿主的索引器
参考以下代码,在创建 `Host_Numbers_Indexer` 类的实例时,它会填充一个包含十个整数的私有数组。该数组的初始内容被转储。然后,通过宿主的下标运算符,索引器将整数 30 通过 `IndexAccessor` 插入到数组的第六个位置,然后将其读回。数组的修改后的内容再次被转储。
Host_Numbers_Indexer test;
cout << "Before input: " << endl
<< "test: " << endl << test << endl;
test[5] = 30;
cout << test[5] << endl;
cout << "After input: " << endl
<< "test: " << endl << test << endl;
输出
这是上述代码的控制台输出。
Before input:
test:
0 1 4 9
16 25 36 49
64 81
30
After input:
test:
0 1 4 9
16 30 36 49
64 81
结论
通过逆向工程 C# 的属性和索引器,我更深刻地体会到了 C++ 提供的灵活性。尽管在 C# 中使用这些特性可能非常直接,但我发现将这些代码组合在 C++ 中是一项非常引人入胜的练习。