Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Implementing VB/C# Style Properties in C++ Classes

3.92/5 (5 votes)
24 Apr 2006CPOL5 min read 1  
VB.NET and C# offer property procedures that execute code when the value of a property is set or retrieved. With novel use of code, C++ classes can have them too!

Contents

Introduction

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!

Background

For a quick review, let's look at a sample property procedure in VB.NET:

VB
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.

VB
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.

Proposition

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.

The Pointer-to-Member Operator

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:

C++
class CPropertyUserBase {}; // Define an empty class 

int CPropertyUserBase::*pInt;
// Declare a pointer to an int member
// of an arbitrary instance of the CPropertyUserBase class.

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:

C++
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.

Operator Overloading

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.

Solution Using Inheritance

Combining these tools, we can define a class that emulates the behavior that property procedures provide in other languages:

C++
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.
        }
    }
};

To use the class, we will need to do three things:

  1. Inherit from CPropertyUserBase.
  2. Define delegate functions that match the signature expected by the CIntProperty class.
  3. Call the Initialize of each CIntProperty object with the appropriately case pointers.

Here's a C++ example of the Thermostat class that uses CIntProperty:

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);
    }
};

Using our CThermostat class thus becomes as simple as was our VB.NET example:

C++
CThermostat Livingroom;
LivingRoom.Fahrenheit = 80;
cout << "The temperature in Celsius is " << (int)(LivingRoom.Celsius) << endl;

Generalizing the Solution with Templates

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.

C++
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.
        }
    }
};

With our new, generic, template-wielding CProperty class in hand, we can now create a CThermostat class that looks like this:

C++
// 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);
    }
};

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.

Conclusion

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)