Contents
One of my favorite tools to use in the .NET languages is the property procedure. Property procedures provide the ability to:
- hide data the user should not have direct access to.
- translate data from one value or data type to another.
- limit the range of acceptable values by filtering or generating a trappable error.
- eliminate unnecessary complexity in class interface design.
Unfortunately, the C++ language does not provide such a language construct. Having said that, however, we can trick C++ into behaving as though property procedures are an every day part of the language!
For a quick review, let's look at a sample property procedure in 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
In our example class, temperature is stored internally as Kelvin (presumably because other parts of the class or application use the value in Kelvin). Through the use of a property procedure, however, we've provided a simple, efficient, and clean conversion between Fahrenheit or Celsius and Kelvin. For demonstration, the class also changes data types between the internally stored Double
and the exposed Integer
properties.
Dim LivingRoom As Thermostat
LivingRoom.Fahrenheit = 80
Console.WriteLine "The temperature in Celsius is " & CStr(LivingRoom.Celsius)
As demonstrated, the class provides a clean interface with a simple, straightforward usage. As far as the class user is concerned, the value of a member variable is being changed and the value of another member variable is being read. The complexity of what goes on behind the scenes (inside the class) is completely hidden.
Providing such an interface for classes in C++ is more difficult, but not too much so. To do it, we will need to create a CProperty
class that uses at least two tools:
- pointer-to-member operator
- operator overloading
These tools will allow us to design our class in such a way that it behaves as both a data type and as the invoker of delegate functions that are defined in the user of our class. We will also need to use class inheritance or class templates, or some combination of the two technologies.
MSDN's C++ Language Reference provides the following definition:
"The pointer-to-member operators, .* and *, return the value of a specific class member for the object specified on the left side of the expression."
It all sounds simple and painless until you start looking at the code samples provided in the MSDN help. In my opinion, the complexity of its use has to do more with the way a pointer-to-member variable is declared and dereferenced than the actual use of the operator. Let's take a look at a pointer-to-member variable declaration:
class CPropertyUserBase {};
int CPropertyUserBase::*pInt;
The declaration above looks a little odd at first, but makes sense. It combines the typical declaration for an int pointer int *pInt;
with the scope resolution operator CPropertyUserBase::
to declare a pointer to an int
member of an instance of a CPropertyUserBase
class.
The fact that our class definition contains no actual int
members (or no members at all, for that matter) is irrelevant. With proper type casting, the pointer-to-member variable can just as easily be made to point to an int
member of some derived class. Further, we can extend the concept from pointers to data members of a class to pointers to functions of a class:
int (CPropertyUserBase::*pSomeMemberFunc)(const int);
The line above declares a pointer to a member function of CPropertyUserBase
that takes an integer argument and returns an integer value.
Our ultimate goal is, of course, to allow our CProperty
class to behave like another data type. To do that, we must, at a minimum, overload the assignment and type conversion operators for the class. To fully implement operators for the class, however, other binary operators such as +=
must be overloaded as well. Most C++ programmers are likely familiar with operator overloading, so I won't go into detail on its use here. Suffice to say that overloading operators for a class allows us to redefine how the compiler treats objects of that class, allowing us to use them as we would any other intrinsic data type such as an int
or double
.
Combining these tools, we can define a class that emulates the behavior that property procedures provide in other languages:
class CPropertyUserBase {};
class CIntProperty
{
int Value;
CPropertyUserBase *pPropertyUser;
int (CPropertyUserBase::*pOnSetValueFunc)(const int);
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)
{
Value = InitValue;
pPropertyUser = pObj;
pOnSetValueFunc = pSetFunc;
pOnGetValueFunc = pGetFunc;
}
int operator =(const int rhs)
{
Value = rhs;
if (pPropertyUser && pOnSetValueFunc)
{
Value = (pPropertyUser->*pOnSetValueFunc)(rhs);
}
else
{
Value = rhs;
}
return Value;
}
operator int()
{
if(pPropertyUser && pOnGetValueFunc)
{
return (pPropertyUser->*pOnGetValueFunc)(Value);
}
else
{
return Value;
}
}
};
To use the class, we will need to do three things:
- Inherit from
CPropertyUserBase
. - Define delegate functions that match the signature expected by the
CIntProperty
class. - Call the
Initialize
of each CIntProperty
object with the appropriately case pointers.
Here's a C++ example of the Thermostat class that uses CIntProperty
:
class CThermostat : CPropertyUserBase
{
private:
double Kelvin;
public:
CIntProperty 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:
CIntProperty 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,
(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);
}
};
Using our CThermostat
class thus becomes as simple as was our VB.NET example:
CThermostat Livingroom;
LivingRoom.Fahrenheit = 80;
cout << "The temperature in Celsius is " << (int)(LivingRoom.Celsius) << endl;
The example also suffers from two problems:
- The definition is specific to a single data type.
- The implementation is aesthetically complex.
Both problems can best be solved using class templates. In the first case, we will use a template parameter where we had previously used an int
declaration. In the second case, we will eliminate the need for inheritance entirely by replacing the base class name with another template parameter. This has the added benefit of eliminating the ugly looking type casting when we call the Initialize
function. The down-side is potential code bloat. With each new template declaration that uses a different object, the compiler generates a new set of code. A fair middle ground would keep the base class to inherit from and use a template parameter for the data type. But, I'll leave that as an exercise for the reader.
template <typename PROP_USER, typename PROP_DATA_TYPE>
class CProperty
{
private:
PROP_DATA_TYPE Value;
PROP_USER *pPropertyUser;
PROP_DATA_TYPE (PROP_USER::*pOnSetValueFunc)(const PROP_DATA_TYPE);
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)
{
Value = InitValue;
pPropertyUser = pObj;
pOnSetValueFunc = pSetFunc;
pOnGetValueFunc = pGetFunc;
}
PROP_DATA_TYPE operator =(const PROP_DATA_TYPE rhs)
{
Value = rhs; if (pPropertyUser && pOnSetValueFunc)
{
Value = (pPropertyUser->*pOnSetValueFunc)(rhs);
}
else
{
Value = rhs;
}
return Value;
}
operator PROP_DATA_TYPE()
{
if(pPropertyUser && pOnGetValueFunc)
{
return (pPropertyUser->*pOnGetValueFunc)(Value);
}
else
{
return Value;
}
}
};
With our new, generic, template-wielding CProperty
class in hand, we can now create a CThermostat
class that looks like this:
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);
}
};
As promised, the need to inherit from a base class is gone, as well as the ugly looking type cast operations that were previously needed when calling the Initialize
method.
The template class provided above may be used as is, but is far from a finished product. It is simply a foundation from which to build, with plenty of room for polishing and refinement. Some suggested improvements might be:
- Implement all reasonable operators.
- Supply a defined copy constructor function.
- Apply the
const
keyword more stringently than I have in my examples (ya, I know...I'm bad about lax use of const
). - Explore the potential need to pass and return a reference in assignment operator functions.
Enjoy!
History
- Original post - April 24, 2006.