在 C++ 类中实现 VB/C# 风格的属性






3.92/5 (5投票s)
VB.NET 和 C# 提供了属性过程,它们在属性值被设置或检索时执行代码。通过新颖的代码使用方式,C++ 类也可以拥有它们!
目录
引言
我最喜欢的 .NET 语言工具之一是属性过程。属性过程提供了
- 隐藏用户不应直接访问的数据。
- 将数据从一种值或数据类型转换为另一种。
- 通过过滤或生成可捕获的错误来限制可接受值的范围。
- 消除类接口设计中不必要的复杂性。
不幸的是,C++ 语言不提供这样的语言构造。尽管如此,我们仍然可以“欺骗”C++,使其表现得好像属性过程是语言中日常的一部分!
背景
为了快速回顾,让我们来看一个 VB.NET 中的示例属性过程
Class Thermostat
Private Kelvin As Double
Public Property Celsius() As Integer
Get
Return CInt(Kelvin - 273.15)
End Get
Set(ByVal Value As Integer)
Kelvin = Value + 273.15
End Set
End Property
Public Property Fahrenheit() As Integer
Get
Return CInt(((Kelvin - 273.15) * 1.8) + 32)
End Get
Set(ByVal Value As Integer)
Kelvin = ((Value - 32) * (5 / 9)) + 273.15
End Set
End Property
End Class
在我们的示例类中,温度内部以开尔文 (Kelvin) 存储(假设因为类或应用程序的其他部分使用开尔文值)。然而,通过使用属性过程,我们提供了摄氏度或华氏度与开尔文之间的简单、高效、清晰的转换。为了演示,该类还在内部存储的 Double
和公开的 Integer
属性之间转换数据类型。
Dim LivingRoom As Thermostat
LivingRoom.Fahrenheit = 80
Console.WriteLine "The temperature in Celsius is " & CStr(LivingRoom.Celsius)
如示例所示,该类提供了一个干净的接口,具有简单、直接的用法。就类用户而言,他们正在改变一个成员变量的值,并且正在读取另一个成员变量的值。幕后发生的事情(类内部)的复杂性被完全隐藏了。
提案
为 C++ 中的类提供这样的接口会更困难,但并非过于困难。为此,我们将需要创建一个 `CProperty` 类,该类至少使用两种工具:
- 指向成员运算符
- 运算符重载
这些工具将允许我们以某种方式设计我们的类,使其表现得既像一种数据类型,又像调用我们类用户定义的委托函数。我们还需要使用类继承或类模板,或者两者的某种组合。
指向成员运算符
MSDN 的 C++ 语言参考提供了以下定义:
“指向成员的运算符 `.*` 和 `->*` 返回表达式左侧指定的对象的特定类成员的值。”
这一切听起来简单而轻松,直到你开始查看 MSDN 帮助中提供的代码示例。在我看来,其使用复杂性更多地在于指向成员变量的声明和解引用的方式,而不是运算符的实际使用。让我们来看一个指向成员变量的声明:
class CPropertyUserBase {}; // Define an empty class
int CPropertyUserBase::*pInt;
// Declare a pointer to an int member
// of an arbitrary instance of the CPropertyUserBase class.
上面的声明乍一看有点奇怪,但有其道理。它将 `int` 指针的典型声明 `int *pInt;` 与作用域解析运算符 `CPropertyUserBase::` 结合起来,以声明一个指向 `CPropertyUserBase` 类实例的 `int` 成员的指针。
事实上,我们的类定义不包含任何实际的 `int` 成员(或者根本不包含任何成员)是无关紧要的。通过正确的类型转换,指向成员的指针可以同样容易地指向某个派生类的 `int` 成员。此外,我们可以将这个概念从指向类数据成员的指针扩展到指向类函数的指针。
int (CPropertyUserBase::*pSomeMemberFunc)(const int);
上面的行声明了一个指向 `CPropertyUserBase` 的成员函数的指针,该函数接受一个整数参数并返回一个整数值。
运算符重载
我们的最终目标当然是让我们的 `CProperty` 类表现得像另一种数据类型。要做到这一点,我们至少必须为该类重载赋值运算符和类型转换运算符。然而,要完全实现类的运算符,还必须重载其他二元运算符,如 `+=`。大多数 C++ 程序员可能熟悉运算符重载,因此我在这里不会详细介绍它的用法。可以说,为类重载运算符允许我们重新定义编译器如何处理该类的对象,从而允许我们将它们像任何其他内在数据类型(如 `int` 或 `double`)一样使用。
使用继承的解决方案
通过组合这些工具,我们可以定义一个类,该类模拟其他语言中属性过程提供的行为。
class CPropertyUserBase {};
// Define an empty base class for its type signature
class CIntProperty
{
// the value to store
int Value;
// pointer to class containing delegate functions
CPropertyUserBase *pPropertyUser;
// offset ptr to Set Value function
int (CPropertyUserBase::*pOnSetValueFunc)(const int);
// offset ptr to Get Value function
int (CPropertyUserBase::*pOnGetValueFunc)(const int);
public:
CIntProperty() { pPropertyUser = NULL; pOnSetValueFunc = NULL;
pOnGetValueFunc = NULL; }
void Initialize(const int InitValue,
CPropertyUserBase *pObj,
int (CPropertyUserBase::*pSetFunc)(int) = NULL,
int (CPropertyUserBase::*pGetFunc)(int) = NULL)
{
// store our initial value
Value = InitValue;
// store pointer to the object
// containing our delegate functions
pPropertyUser = pObj;
// store the offset pointers to these functions
pOnSetValueFunc = pSetFunc;
pOnGetValueFunc = pGetFunc;
}
int operator =(const int rhs)
// overloaded assignment operator
{
// assign the passed-in value to the internal value
Value = rhs;
if (pPropertyUser && pOnSetValueFunc)
// if the delegate object and function pointers are non-zero...
{
Value = (pPropertyUser->*pOnSetValueFunc)(rhs);
// ...then use pointer-to-member operator to call the function
// with the passed in value and assign the result to the internal value.
}
else
{
Value = rhs;
// ...else assign the value passed in to our internal value
}
return Value;
}
operator int()
// overloaded type conversion operator
{
if(pPropertyUser && pOnGetValueFunc)
// if object and function pointers are non-zero...
{
return (pPropertyUser->*pOnGetValueFunc)(Value);
// ...then use pointer-to-member operator to call the function and
// return the result of the function called
// with the current internal value
}
else
{
return Value;
// ...else return the internally stored value.
}
}
};
要使用该类,我们将需要做三件事:
- 从 `CPropertyUserBase` 继承。
- 定义与 `CIntProperty` 类期望的签名匹配的委托函数。
- 使用相应的指针调用每个 `CIntProperty` 对象的 `Initialize` 方法。
下面是使用 `CIntProperty` 的 `Thermostat` 类的 C++ 示例:
// Example 1
class CThermostat : CPropertyUserBase
{
private:
double Kelvin;
public:
CIntProperty Celsius;
private:
int Celsius_GetValue(const int value)
{
// Remember that our Celsius object has an internally stored int value
// that gets passed to this function, but we've chosen to simply ignore it.
return ((int)(Kelvin - 273.15));
}
int Celsius_SetValue(const int value)
{
Kelvin = (double)value + 273.15;
return (int)Kelvin;
}
public:
CIntProperty Fahrenheit;
private:
int Fahrenheit_GetValue(const int value)
{
// Remember that our Fahrenheit object has an internally stored int value
// that gets passed to this function, but we've chosen to simply ignore it.
return (int)(((Kelvin - 273.15) * 1.8) + 32);
}
int Fahrenheit_SetValue(const int value)
{
Kelvin = (((double)value - 32) * (5 / 9)) + 273.15;
return (int)Kelvin;
}
public:
CThermostat()
{
Celcius.Initialize(0,
this,
(int (CPropertyUserBase::*)(const int))Celsius_SetValue,
(int (CPropertyUserBase::*)(const int))Celsius_GetValue);
Fahrenheit.Initialize(0,
this,
(int (CPropertyUserBase::*)(const int))Fahrenheit_SetValue,
(int (CPropertyUserBase::*)(const int))Fahrenheit_GetValue);
}
};
使用我们的 `CThermostat` 类变得像我们的 VB.NET 示例一样简单:
CThermostat Livingroom;
LivingRoom.Fahrenheit = 80;
cout << "The temperature in Celsius is " << (int)(LivingRoom.Celsius) << endl;
使用模板泛化解决方案
该示例还存在两个问题:
- 定义特定于单一数据类型。
- 实现美学上很复杂。
这两个问题都可以通过类模板来最好地解决。在第一种情况下,我们将使用一个模板参数来代替我们之前使用的 `int` 声明。在第二种情况下,我们将通过用另一个模板参数替换基类名来完全消除对继承的需要。这还有消除调用 `Initialize` 函数时那些看起来很丑陋的类型转换的好处。**缺点是潜在的代码膨胀**。对于使用不同对象的每个新模板声明,编译器都会生成一套新的代码。一个折衷的中间方案是保留要继承的基类,并为数据类型使用模板参数。但我将把这个留给读者作为练习。
template <typename PROP_USER, typename PROP_DATA_TYPE>
class CProperty
{
private:
// the value to store
PROP_DATA_TYPE Value;
// pointer to class containing delegate functions
PROP_USER *pPropertyUser;
// offset ptr to Set Value function
PROP_DATA_TYPE (PROP_USER::*pOnSetValueFunc)(const PROP_DATA_TYPE);
// offset ptr to Get Value function
PROP_DATA_TYPE (PROP_USER::*pOnGetValueFunc)(const PROP_DATA_TYPE);
public:
CProperty() { pPropertyUser = NULL; OnSetValue = NULL; OnGetValue = NULL; }
void Initialize(const PROP_DATA_TYPE InitValue,
PROP_USER *pObj,
PROP_DATA_TYPE (PROP_USER::*pSetFunc)(PROP_DATA_TYPE) = NULL,
PROP_DATA_TYPE (PROP_USER::*pGetFunc)(PROP_DATA_TYPE) = NULL)
{
// store our initial value
Value = InitValue;
// store pointer to the object containing our delegate functions
pPropertyUser = pObj;
// store the offset pointers to these functions
pOnSetValueFunc = pSetFunc;
pOnGetValueFunc = pGetFunc;
}
PROP_DATA_TYPE operator =(const PROP_DATA_TYPE rhs)
// overloaded assignment operator
{
Value = rhs; // assign the passed-in value to the internal value
if (pPropertyUser && pOnSetValueFunc)
// if the delegate object and function pointers are non-zero...
{
Value = (pPropertyUser->*pOnSetValueFunc)(rhs);
// ...then use pointer-to-member operator to call the function
// with the passed in value and assign the result to the internal value.
}
else
{
Value = rhs;
// ...else assign the value passed in to our internal value
}
return Value;
}
// Overloaded type-conversion operator passes
// the current internally stored value to the delegate
// function and returns the value passed back from the delegate function
operator PROP_DATA_TYPE()
// overloaded type conversion operator
{
if(pPropertyUser && pOnGetValueFunc)
// if object and function pointers are non-zero...
{
return (pPropertyUser->*pOnGetValueFunc)(Value);
// ...then use pointer-to-member operator to call the function and
// return the result of the function called with the current internal value
}
else
{
return Value;
// ...else return the internally stored value.
}
}
};
有了我们新的、通用的、使用模板的 `CProperty` 类,我们现在可以创建一个 `CThermostat` 类,它看起来像这样:
// Example 2
class CThermostat
{
private:
double Kelvin;
public:
CProperty<CThermostat, int> Celsius;
private:
int Celsius_GetValue(const int value)
{
return ((int)(Kelvin - 273.15));
}
int Celsius_SetValue(const int value)
{
Kelvin = (double)value + 273.15;
return (int)Kelvin;
}
public:
CProperty<CThermostat, int> Fahrenheit;
private:
int Fahrenheit_GetValue(const int value)
{
return (int)(((Kelvin - 273.15) * 1.8) + 32);
}
int Fahrenheit_SetValue(const int value)
{
Kelvin = (((double)value - 32) * (5 / 9)) + 273.15;
return (int)Kelvin;
}
public:
CThermostat()
{
Celcius.Initialize(0, this, Celsius_SetValue, Celsius_GetValue);
Fahrenheit.Initialize(0, this, Fahrenheit_SetValue, Fahrenheit_GetValue);
}
};
正如所承诺的,不再需要从基类继承,以及之前调用 `Initialize` 方法时需要的那种丑陋的类型转换操作。
结论
上面提供的模板类可以直接使用,但远非最终产品。它仅仅是一个构建基础,有大量的改进和完善空间。一些建议的改进可能包括:
- 实现所有合理的运算符。
- 提供一个定义的复制构造函数。
- 比我在示例中更严格地使用 `const` 关键字(是的,我知道……我在宽松地使用 `const` 方面做得不好)。
- 探讨在赋值运算符函数中传递和返回引用的潜在需求。
尽情享用!
历史
- 原帖 - 2006 年 4 月 24 日。