Delen via


Tracking C++ variable state changes

Tracking class variables' state changes can be tricky, especially if we are using too many asynchronous constructs.
This is especially true for game and graphic application scenarios where
hundreds and perhaps even thousands of object fly around changing their values
in who knows which thread. (Who is forcing that character position into that map
corner - physics thread or AI thread?) Couple of years back I wrote few tiny classes to address this problem as part of my Object Intropsection Library (OIL). Here are few details of
it. Go through them and feel free to use them as you see fit for your application.

While a class can be a complex datastructure, its internal members in the end have to be simple data types such as int, char, float, double etc... (For our present purpose we ignore any classes that fall outside this category).
The state of a class object thus encompasses the state of each of its individual simple members. Being able to track the invidiual member changes provides one way of tracking the state change for the entire object.
On the otherhand, when a class is more mathemtical oriented (such as Vector3 or Quaternion etc...)
with the functionality built more into its operators than into its methods, then tracking
its "mutation" operators shall give us indication about the state change of the object.

We can combine these two and track a complex object by tracking its internal "suspect" variables, which by their nature (of being simple datatypes) can only change their value through operators. This might sound a little but clumsy, but if we take a careful look at the
complete list of C++ operators, we have only a handful of them that are "value changing" kind. Below are they:

      ///<Summary>
    /// Enumeration Constants for the Function Context of Value-Changes;
    /// </Summary>
    /// <Remarks>
    /// Note that we override only the functions (and operators) that trigger a value 
    /// change for our CProperty object. Thus, operators such as &, &&, || etc. are not 
    /// overloaded as they do not change our object's content.
    /// </Remarks>
    enum enumFunContext
    {
       FUN_UNKNOWN = -1,
       FUN_CONSTRUCTOR,     ///< Constructor
       FUN_COPYCONSTRUCTOR, ///< Copy Constructor
       FUN_DESTRUCTOR,      ///< Destructor
       OP_ASSIGN,           ///< Operator =
       OP_PLUSASSIGN,       ///< Operator +=
       OP_MINUSASSIGN,      ///< Operator -=
       OP_STARASSIGN,       ///< Operator *=
       OP_SLASHASSIGN,      ///< Operator /=
       OP_MODASSIGN,        ///< Operator %=
       OP_ANDASSIGN,        ///< Operator &=
       OP_ORASSIGN,         ///< Operator |=
       OP_XORASSIGN,        ///< Operator ^=
       OP_LSHIFTASSIGN,     ///< Operator <<=
       OP_RSHIFTASSIGN,     ///< Operator >>=
       OP_PREINCREMENT,     ///< Operator ++
       OP_PREDECREMENT,     ///< Operator --
       OP_POSTINCREMENT,    ///< Operator ++(int)
       OP_POSTDECREMENT,    ///< Operator --(int)
    };           

Thus we can define a simple templated class as shown below that can override these operators for us.

      template<typename DATATYPE>
    class CProperty : public IsClassType<DATATYPE>::ResultantType, public CEventSource
    {
    public:
        typedef typename IsClassType<DATATYPE>::ResultantType ClassType;
        typedef typename IsPredefinedType<DATATYPE>::ResultantType PredefinedType;

        enum { IsClass = Loki::TypeTraits<DATATYPE>::isClass };
        typedef Loki::Int2Type<IsClass> TypeSelector;

        typedef CValueChangingEventArgs<DATATYPE> ChangingEvArgs;
        typedef CValueChangedEventArgs<DATATYPE> ChangedEvArgs;

    protected:
        PredefinedType m_Data;       // Create the Object for Predefined Types

    public:
        CEvent   ValueChanging;      ///< Event Raised before the value changes

        CEvent  ValueChanged;       ///< Event Raised after the value changed
        
        ...
    };

The above shown CProperty templated class lets us define member variables as Properties in C++ classes, in that it allow us to have "value changing" and "value changed" events notified for those Property variables.
The ValueChanging event is raised when the value is about to change, while the
ValueChanged is raised when the change is over. These keep track of the data value changes by handling the afore discussed operator functions.

The problem, however, is that we should be able to track the changes for both pre-defined types (such as int, float etc...) and for the user-defined class types.
For native types the C++ compiler automatically allows all the above defined operators and hence we
don't have a problem with them. But it is the user-defined class types that
we have a problem with - because not all user-defined class types will have all these operators defined on them
!! For example, I have never seen a string class having the operator*= defined
for it !! (and pray god we don't see one in future either)

To handle these undefined cases, CProperty class defines the operators as templated functions, which on many
a good C++ compilers will be ignored and will not be processed further, unless the objects actually use them (a good indication that the given class has indeed defined them).

For an user-defined class type, we keep track of the changes for the class objects by making the CProperty a derived class for the given class. Thus we get a chance to handle the operators before the base class takes a look at it.
However, if the given DataType is not a class then we could not use this strategy of deriving from it. (What does it mean to derive from a viod* ??)

Thus, in case of given DataType being a class type, we derive from it. In all other cases, we actually create an object of that type and use CProperty as
a wrapper over it.
Making CProperty a derived class is necessary because objects of the given DataType should be able to call any extra functions that
might be present in that type in their class (but not defined here in CProperty class). Since non-class types (such as pre-defined types and pointer types) do not
have that necessity, it is enough to wrap over those objects.

We use the IsClassType<DATATYPE> mechanism to determine if the give DATATYPE is a class type or not. It is a template meta-programming construct defined as below:

      /// <Summary>
    /// We should be able to invoke Copy Constructor on the Empty type.
    /// So we define it here. Loki::EmptyType does not have Copy Constructor;
    /// </Summary>
    class EmptyType 
    {
    public:
        template<typename T>
        EmptyType(const T& ) { }
        EmptyType() { }
    };

    /// <Summary>
    /// IsClassType -- Tests if the given DATATYPE is a Class or not.
    /// If the DATATYPE is a Class then ResultantType is set to DATATYPE;
    /// Else ResultantType is set to EmptyType;
    /// </Summary>
    template<typename DATATYPE>
    struct IsClassType
    {
        typedef typename Loki::Select
            < 
                Loki::TypeTraits<DATATYPE>::isClass == true, 
                DATATYPE,
                EmptyType 
            >::Result ResultantType;
    };     

The next point to consider is that among the operators we have considered, there are unary operators (such as ++, -- etc...) and then there are binary operators (such as +=, = etc...). We consider each individually. In fact, we can have a single generic implementation for all binary operators
with a simple macro as below:

 #define OVERLOAD_BINARY_OPERATOR(operator_func, DATA_OP, OP_FUNC_CTX)    \
    protected:\
        template<typename T>                      \
        inline void Func_##OP_FUNC_CTX(const T& other, Loki::Int2Type<true>&)    \
        {\
            ChangingEvArgs evArgs((DATATYPE)*this, OP_FUNC_CTX); \
            RaiseEvent(&ValueChanging, &evArgs);\
            if(evArgs.GetCancel() == false)\
            {\
                ClassType::operator_func(other);\
                ChangedEvArgs evArgs((DATATYPE)*this, OP_FUNC_CTX);    \
                RaiseEvent(&ValueChanged, &evArgs);\
            }\
        }\
        template<typename T>\
        inline void Func_##OP_FUNC_CTX(const T& other, Loki::Int2Type<false>∧&)\
        {\
            ChangingEvArgs evArgs(m_Data, OP_FUNC_CTX);\
            RaiseEvent(&ValueChanging, &evArgs);\
            if(evArgs.GetCancel() == false)\
            {\
                DATATYPE OldValue = m_Data;\
                m_Data DATA_OP (DATATYPE)(*const_cast<T*>(&other));\
                ChangedEvArgs evArgs(OldValue, m_Data, OP_FUNC_CTX);\
                RaiseEvent(&ValueChanged, &evArgs);\
            }\
        }\
        template<typename T, typename ObjType>\
        inline void Func_##OP_FUNC_CTX(const T& other, Loki::Type2Type<ObjType> &)\
        {\
            Func_##OP_FUNC_CTX(other, TypeSelector());\
        }\
        template<ypename T, typename ObjType>\
        inline void Func_##OP_FUNC_CTX(const T& other, Loki::Type2Type<CProperty<ObjType> >&)\
        {\
            Func_##OP_FUNC_CTX((ObjType)other, TypeSelector());\
        }\
    public:\
        template<typename T>\
        inline CProperty& operator_func(const T& other)\
        {\
            Func_##OP_FUNC_CTX(other, Loki::Type2Type<T>());\
            return *this;\
        }        

With this we can now define all the binary operators simply as:

      /////////////////////////////////////////
    /// Assignment Operator
    /////////////////////////////////////////
    OVERLOAD_BINARY_OPERATOR(operator=, =, OP_ASSIGN);

    /////////////////////////////////////////
    /// Plus-Assign += Operator
    /////////////////////////////////////////
    OVERLOAD_BINARY_OPERATOR(operator+=, +=, OP_PLUSASSIGN);

    /////////////////////////////////////////
    /// Minus-Assign -= Operator
    /////////////////////////////////////////
    OVERLOAD_BINARY_OPERATOR(operator-=, -=, OP_MINUSASSIGN);
        
    ...        

As for the unary operators, a typical implementation would look like below:

      protected:
        /////////////////////////////////////////
        /// PostIncrement Operator
        /////////////////////////////////////////
        template<bool bIsClass>
        inline void OpPostIncrement()
        {
            ChangingEvArgs evArgs((DATATYPE)*this, OP_POSTINCREMENT);
            RaiseEvent(&ValueChanging, &evArgs);
            if(evArgs.GetCancel() == false)
            {
                ClassType::operator++(0);
                ChangedEvArgs evArgs((DATATYPE)*this, OP_POSTINCREMENT);
                RaiseEvent(&ValueChanged, &evArgs);
            }
        }
        template<>
        inline void OpPostIncrement<false>()
        {
            ChangingEvArgs evArgs(m_Data, OP_POSTINCREMENT);
            RaiseEvent(&ValueChanging, &evArgs);
            if(evArgs.GetCancel() == false)
            {
                DATATYPE OldValue = m_Data;
                m_Data++;
                ChangedEvArgs evArgs(OldValue, m_Data, OP_POSTINCREMENT);
                RaiseEvent(&ValueChanged, &evArgs);
            }
        }  
    public:
        inline CProperty& operator ++()
        { 
            OpPreIncrement<IsClass>();
            return *this;
        }           

Note that we define each operator in two flavors, one for the pre-defined types and the other for the user-defined type. The selection is done automatically by the compiler based on the result of IsClass enum value defined in the CProperty class.
For example, in the above code, when operator++ is invoked, it will be routed to the OpPostIncrement<true> or OpPostIncrement<false> based on OpPostIncrement<IsClass>. The difference is, for the class types we call the base class's ClassType::operator++, where as for native types we just do m_Data++ on the wrapped object.

Equipped with this mechanism, we can now track the changes to a variable by declaring it to be of CProperty type and subscribing to it's
valuechanged (and/or valuechanging) events as shown below:

    // Define the variable
   CProperty<int> Obj1 = 20;
 
    // Add a listener for it
 Obj1.ValueChanged.Subscribe(&HandleValueChanged_Func);
  
    // Define the Listener
   void HandleValueChanged_Func(const CEventSource* EvSrc,CEventHandlerArgs* EvArgs)
   {       
      CValueChangedEventArgs<int>* pEvArgs = (CValueChangedEventArgs<int>*) EvArgs;
   TCHAR sz[256];
      _stprintf(sz, _T("Value Changed from %d to %d"), pEvArgs->OldValue(), pEvArgs->NewValue());
   MessageBox(NULL, sz, _T("Change Event"), MB_OK);
  }       

Once the event is raised, there is no restriction on what you can
do inside the event handler. A log entry can generated, or debug break can be
called or notification can be displayed or ... what not.

Tracking the member variables of class is similar. Just declare the member variable to be of CProperty type:

  //////////////////////////////
/// A class that uses CProperty to track its member variables
class CTestClass
{
public:
    CProperty<int> Value; // A member variable that is of type CProperty
    ...
};  

int _tmain(int argc, _TCHAR* argv[])
{
    // Declare the object 
    CTestClass var;
   
    // Subscribe to the notifications for interested variables
    var.Value.ValueChanging.Subscribe(&HandleValueChanged_Func);
   
    var.Value = 10; // Raises the notification
}  

Tracking the changes for the instances of class is also similar. Just declare the class object to be of CProperty type:

  //////////////////////////////
/// A class that we use CProperty against to track its instance variables
class CTestInt
{
    int m_nVal;
public:
    CTestInt() { }
    CTestInt(int nVal) : m_nVal(nVal) {}
    template<typename T>
    CTestInt& operator=(const T& nVal) { m_nVal = (int)nVal; return *this; }
    CTestInt& operator++() { ++m_nVal; return *this;   }
    CTestInt& operator--() { --m_nVal; return *this;   }
    operator int() { return m_nVal;    }
    operator float(){ return (float)m_nVal; }
    operator char() { return (char)m_nVal; }
    ...
}; 
    
int _tmain(int argc, _TCHAR* argv[])
{
    // Declare an instace of the class
    CProperty<CTestInt> Obj1 = 20;  
    
    // Add listener for Obj1
    Obj1.ValueChanged.Subscribe(&HandleValueChanged_Func);
       
    Obj1 = 10; // Raises the notification 
}  

More elaborate usage examples can be found in the attached project.

If you are using your class in too many places then converting
the instances to CProperty in all places can be difficult, especially when
you have method signatures using that class type. In such cases you can do a little trickery. Just wrap your class in a temporary namespace and declare a custom typedef in the usual scope.
For example, to track the instace variables of the CTestInt class shown above, we can wrap the CTestInt in a namespace and define a custom typedef as shown below:

 /// Wrap the class into a custom namespace
namespace _MyTracking
{
    class CTestInt
    {
        int m_nVal;
    public:
        CTestInt() { }
        ...
    };    
}
/// Declare the typedef to make the CTestInt instances to be a type of CProperty
typedef CProperty<_MyTracking::CTestInt> CTestInt;

Now any code that previously was CTestInt
now automatically starts using the CProperty type !! This is quite useful when you are passing your CTestInt
objects across many methods and you don't want to change their function
signatures. Ofcourse, when you are done with the tracking, you can remove the
namespace and the typedef and your code is back to normal!!

As usual, this is yet a simple beginning and you can do wonders by
extending the design. For example, when a "value changing" notification is raised, we can always have the option of "cancelling" the change !!

Go ahead, explore the options and extend the design. Have fun.

TrackObject.zip