Click here to Skip to main content
15,879,239 members
Articles / Desktop Programming / MFC
Article

Application Configuration File Variables

Rate me:
Please Sign up or sign in to vote.
3.18/5 (8 votes)
12 Apr 20048 min read 44.5K   795   12   5
Type-safe access to read-only or read/write configuration file variables using "one-line-of-code".

Sample Image - AppVar.jpg

Introduction

This tiny little tool is a small C++ utility that makes creating and accessing application configuration variables a breeze. In most cases, storing these values in the registry may be more efficient, but for this particular project, the configuration files had to be generated off-site and uploaded onto a remote machine running the application.

During development, each time we needed a new variable, we had to generate the code to declare it, initialize it, save it, and access it. In other words, there was a routine to the whole process which, if a step was left out, could produce unnecessary debugging work.

Wouldn’t it be great if all that I needed to do to generate a new variable was write a single line of code?

Problem Definition

  1. Generate new application config variables, writing “one-line-of-code”, which would do the following:
    • Declare variable by name.
    • Declare variable's type.
    • Contain default value (in case it is not found in the file).
    • Specify the access rights assigned to the variable, i.e. read-only or read/write, and
    • Generate information about the variable for offsite maintenance.
  2. Access the value of the config variable by declaring a local variable whose type resembles the variable name, and by using C++ overloaded operators on the local variable. This would give us the following benefits:
    • If I tried to assign a value to a read only config variable, the compiler would pick this up.
    • If I tried to assign the wrong type to a variable, the compiler would again pick it up.

    E.G.:

    //…somewhere in the project 
    // ("One –line-of-code")DECL_APPVAR_GET(const char*,
    // MyVar, "I have no imagination.") 
    
    // …elsewhere in the project 
    void func() 
    { 
     MyVar var;
    // compiler error – read-only (GET) 
      var = "Wait a minute… I felt something.";
    // compiler error – wrong type
      BOOL eg = var;  
    // OK to read const char*
     AfxMessageBox(var);
    }
  3. Stream the variable(s).

Solution

I began by looking at the syntax that we would like to use for accessing the variable. I saw two possibilities:

  • CAppVar::MyVar var; // or
  • CAppVar<MyVar> var;

At first, I thought the second syntax looked the most appealing. However, because it’s a template, serialization would be a little more difficult. I wanted to keep the solution as simple as possible, so I opted for the first syntax.

Next came the “one-line-of-code” macro that would generate the bulky output, which I wanted to avoid.

Obviously, this is going to need some thinking. In terms of C++, thinking comes in classes so we need:

  • Wrapper class – This object will contain the information about a single variable together with the capability to stream the information and read or write a value to it.
  • Accessor class – This class will be used to access the value of the variable from anywhere in the application using the declared access rights and type.
  • Initializer class – This object will initialize all the necessary information for us automatically upon creation.
  • Container class – This object will house all our variables.

Our “one-line-of-code” macro will generate most of this code for us and distribute the information accordingly.

Warning: Some programmers believe that this style of macro-coding is hard to read and maintain. While I don’t completely disagree, I do believe that if you understand the underlying code and why it is implemented using macros, then there is no reason not to use them in this fashion. Macros were designed just for that and hence named “MACROS”, not “DEFINES” ;)

Let's start by looking at the wrapper class and what kind of information we can get to and from it:

The Wrapper Class

The wrapper class is called var_info and maintains information about a particular variable. This class mimics a template class, where the data can be accessed using two methods:

  • Simple – where the data is assumed to be of non-pointer type and needs address conversion.
  • String – where the data is assumed to be a pointer and no address translation is required.

The reason this class is not a template is briefly mentioned above and can be summarized as a slight increase in the complexity of RAM data storage for a much reduced complexity in streaming and disk storage.

The class’ data members are as follows:

class var_info : public CObject
{
    DECLARE_SERIAL(var_info)
    friend class CAppVar;
protected:
    // used for lookups
    CString     name;

    // just for show
    CString     type;  // this is only used for offsite reading
    BOOL        write; // this is only used for offsite reading

    // real info and data
    int        size;
    void*        data;

Currently, any lookups are done by name. This is not the most efficient way, but for this example, I did not want to have to provide another piece of information (key) to the declaration macro. Feel free to change this, if necessary. Also, the list is implemented using CObList. Don't laugh, this is left-over from thinking I will be requiring its serialization services. It will be removed.

The class takes advantage of MFC’s serialization techniques (future projects include a port to WTL – when required).

Access to the data is managed by the following members and operators:

// operators
inline operator int()
    { return _simple(int, data); }

inline operator const char*()
    { return _string(const char*, data); }

inline void operator=(UINT d)
    { copy((void*)&d, sizeof(UINT));    }

inline void operator=(int d)
    { copy((void*)&d, sizeof(int));    }

void operator=(const char* d)
{ data = (d) ? copy((void*)d, strlen(d)+1):NULL; size = (data) ? size:0; }

…
// general access
inline const char* GetName()
        { return name; }

inline const char* GetType()
        { return type; }

inline int GetSize()
        { return size; }

inline BOOL AllowWrite()
        { return write; }

…
// serialization
void Serialize(CArchive& ar)
{
    if(ar.IsStoring())
    {
        ar << name << type << write << size;
        ar.Write(data, size);
    }
    else
    {
        ar >> type >> write >> size;
        del(data);
        data = alc(size);
        ar.Read(data, size);
    }
}

…
// helper
CString ToString(); 
// generates a string rep. for the value (needs a better approach)

void ToValue(const char* val); 
//  as above but opposite direction.

protected:
void* copy(void* d, int s)
{
    if(s > size) 
      { del(data); data = alc(s); } 
      // del() == free() && alc() == malloc()
    memcpy(data, d, s);
    size = s;
    return data;
}

The class is fairly straight forward with minimal functionality. Normally, no one accesses this class directly. Any access is managed through the Accessor classes which would in turn access this class according to the access rights specified for that variable. In my example, I have made access to this class available to any one who needs it (an example would be enumerating all the available application variables, as opposed to specific variable access).

Next was the container class:

The Container Class

The container class acts as a wrapper for all the application variables. It maintains an internal static list of var_info structures and is able to match these variables with streaming data. It also serves as both a namespace for our Accessor classes and as the invoking mechanism for the creation and initialization of the application variables. In other words, by creating an instance of this object, our data becomes immediately available. Note: only one instance per application, please.

For this to be achieved, any new application variable must have its “one-line-of-code” added to the body of this class. Here is its declaration for two variables:

class CAppVar : public CObject
{   
// {{ Global Config Variables

    DECL_APPVAR_SETGET(BOOL, MyVar1, FALSE)
    DECL_APPVAR_GET(const char*, MyVar2, _T("I have no imagination."))

// }}

public:
    CAppVar() {}
    virtual ~CAppVar();
    DECLARE_SERIAL(CAppVar)

    void Serialize(CArchive& ar);

public:
    static CObList m_variableList;
    static var_info* Fetch(const char* name, bool assert = true);
};

This particular example has two config variables:

  • MyVar1 – read/write
  • MyVar2 – read-only

The application is responsible for calling the serialization function when needed. All the variables are stored in the m_variableList member upon the container's creation.

Warning: You cannot enter the application with the CAppVar object mapped onto the top stack frame, i.e., it cannot be a direct member of the application object or be declared as a global or static object. In such instances, heap allocation must be used (i.e., a pointer and the new operator). This is due to the order of construction between the static list and the Initializer members.

The other two classes, Accessor and Initializer, are generated by the macros. For the above example, the code would expand to reveal the following CAppVar class (with the exception of some possible copying mistakes). The bold-italic items represent the macro parameters.

C++
class CAppVar : public CObject
{
// {{ Global Config Variables

    // accessor for MyVar1
     class MyVar1
     {
       var_info* info;
     public:
       MyVar1 ()
         { info = Fetch("MyVar1"); ASSERT_VALID(info); }

       void inline operator=(BOOL val)
         { ASSERT_VALID(info); (*info) = val; }
       inline operator BOOL()
         { ASSERT_VALID(info); return (BOOL)(*info); }
       inline const char* GetName()
         { ASSERT_VALID(info); return info->GetName(); }
       inline const char* GetType()
         { ASSERT_VALID(info); return info->GetType(); }
       inline BOOL AllowWrite()
         { ASSERT_VALID(info); return info->AllowWrite(); } \
      };

      private:
      // initializer class
      class CMyVar1
      {
        bool dummy;
      public:
        CMyVar1 ()
          { var_info* inf = new var_info("MyVar1", "BOOL", TRUE); 
            (*inf) = TRUE; m_variableList.AddTail((CObject*)inf); }
      } mMyVar1;

      // accessor for MyVar2
      class MyVar2
      {
        var_info* info;
      public:
        MyVar2 ()
        { info = Fetch("MyVar2"); ASSERT_VALID(info); }
     inline operator const char*()
        { ASSERT_VALID(info); return (const char*)(*info); }
     inline const char* GetName()
        { ASSERT_VALID(info); return info->GetName(); }
     inline const char* GetType()
        { ASSERT_VALID(info); return info->GetType(); }
     inline BOOL AllowWrite()
        { ASSERT_VALID(info); return info->AllowWrite(); }
      };

      private:
      // initializer class
      class CMyVar2
      {
        bool dummy;
      public:
        CMyVar2 ()
        { var_info* inf = new var_info("MyVar2", 
                          "const char*", FALSE); 
          (*inf) = "I have no imagination.";
          m_variableList.AddTail((CObject*)inf); }
     } mMyVar2;

  // }}

public:
  CAppVar() {}
  virtual ~CAppVar();
  DECLARE_SERIAL(CAppVar)

  static void Serialize(CArchive& ar);

public:
  static CObList m_variableList;
  static var_info* Fetch(const char* name, bool assert = true);
};

As you can see from the above, the concept is quite simple. There is nothing to it but wrapping a type declaration around the var_info object with some default parameters and operators. The read-only declaration is missing the assignment operator and will therefore generate a compile error if any attempt was made.

The Accessor is just a public type declaration with the same name as the config variable, while the Initializer class is a member declaration whose constructor is invoked upon the creation of the CAppVar object. This constructor creates a new var_info object with the information provided in the macro and appends it to the CAppVar object's internal list. In my example, no one can access this variable or its type definition – as there is no requirement to.

The Initializer object contains a dummy variable, in case the compiler attempts to optimize the code.

I hope you can see how you are able to access the variable's contents using local declarations of the variable in question. I.e.:

CAppVar::MyVar1 var;
var = TRUE;  // This will set the config variable MyVar1 to TRUE
const char* szTemp = var;  // This will generate a compile error
//

The Example

The provided source code includes an example where the same mechanism was used to create a simple editor for these variables. There is a function called OnExampleCode() which has no entry point but illustrates accessor class usage. I have tried to keep the code as simple as possible without embedding too much in MFC.

This code and example are minimalist implementations. They were created as proto-type code and nothing more. It is not tested extensively and is intended as a 'blank sheet'.

The example allows you to change the values of config variables, save them, or open them - try it and let me know of the problems you encounter.

Conclusion

It’s a relatively simple solution that has no real value other than the time it may save in the creation of new application variables (which can be serialized), and perhaps, as in my case, debugging time from errors in alternate code. It helped us dramatically with both. The only problem is that there usually needs to be a complete re-compilation with each new set of variables added – so plan ahead.

There is probably also a solution like this already out there. I did not see any others nor heard of any, so if there is, then I’m sure all of us - even the most generically inclined – have, at some point, re-invented something.

Also, for my solution, I will be adding an encryption layer before serialization. I may also port this to read registry keys as well, or add some functionality to declare a variable as registry or file.

Written By Alon Azar.

... [F5]

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralError Linking Pin
qcha0s29-Aug-04 5:32
qcha0s29-Aug-04 5:32 
GeneralRe: Error Linking Pin
qcha0s20-Nov-04 12:34
qcha0s20-Nov-04 12:34 
GeneralRe: Error Linking Pin
vv3g20-May-10 23:18
vv3g20-May-10 23:18 
GeneralRe: Error Linking Pin
qcha0s30-May-10 9:37
qcha0s30-May-10 9:37 
Generalorder of initialization Pin
Paolo Messina13-Apr-04 6:52
professionalPaolo Messina13-Apr-04 6:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.