Home

C++ as a Better C

I'm finally back to doing C++ after doing a number of years of web work. It feels a little retro -- Java and C# are all the rage now -- but some programming muscles that haven't been used in a while are finally getting some exercise again.

The bad part is the huge code mess that I've inherited. It's mostly written in a language that I call "C-with-//-comments". Oh, sure, the files have a ".cpp" extension. But the code would compile with a plain C compiler just fine as long as the compiler accepts double-slash comments.

I don't mind going back in time technically five years. But I'm not too happy about going back ten years, to plain C. It's a shame to have the power and convenience of C++ available and to not use it.

I've seen this in a lot of Windows programs. Since the Windows API is all C-based, there's going to be some straight-C somewhere in your code. If it's banished to reusable libraries, as it should be, then great. But if it's peppered throughout your main code, you're using "C with // comments", and that I don't think much of.

Let me give you an example of the problem, and my solution. I've deliberately chosen a tiny example -- this is an article, not a book -- but it should be enough to demonstrate the principles.

C-Style Windows Programming

Suppose we need to check what operating system version we're running on. We need code like the following:

OSVERSIONINFO osi;
::ZeroMemory(&osi, sizeof(osi));
osi.dwVersionInfoSize = sizeof(osi);

if (::GetOSVersionInfo(&osi) == 0)
    // ... check the OS version

This API is used throughout Windows: zero out a structure, set the first DWORD to the structure size, and pass that structure's address on to some function that fills it in.

This is a great way to provide extensibility in the operating system. And the code is perfectly legitimate as C or C++. If you've done enough Windows programming, you probably write code sequences like that all the time.

You've also probably forgotten one or both of the initialization steps, then spent some time scratching your head wondering why your code doesn't work. Wouldn't it be nice if the computer handled the initialization automatically?

It's also unaesthetic. Most of the code is housekeeping that has little to do with the task at hand, namely checking the OS version. There are so many lines of code in proportion to how little work is being done, that it's hard to see the forest for the trees. That creates a maintenance headache.

Let's look at how we can use C++'s features to improve this situation.

Basic Functionality

Usually, the best way to encapsulate an idea is with a class. Let's try that here. We'll wrap a dumb structure into a smart class; we can then write a constructor to guarantee that when we create an instance, it's initialized properly:

class OSVersionInfo
{
public:
    OSVersionInfo()
    {
        ::ZeroMemory(&rec_, sizeof(rec_));
        rec_.dwVersionInfoSize = sizeof(rec_);
    }

    OSVERSIONINFO rec_;
};

Class members ordinarily should be private, not public. But I make an exception for loose wrapper classes such as we're developing here. We're not trying to pretend the structure doesn't exist (doing that is a topic for a different essay). We're just making the structure more convenient and less error prone.

OK, now we never need to initialize an OSVERSIONINFO structure again. But we can't pass an instance of this class to ::GetOSVersionInfo(). Let's fix that by adding an address-of operator:

OSVERSIONINFO* operator&
{
    return &rec_;
}

Because we avoided virtual functions or additional member data, the class is binary-compatible with the structure. So the address of one is equivalent to the address of the other.

Now OSVersionInfo objects will automagically convert to OSVERSIONINFO structures when you take their address:

OSVersionInfo osvi;
if (::GetOSVersionInfo(&osvi) == 0)  // uses OSVersionInfo::operator&() to get ptr to OSVERSIONINFO
    // ... check the OS version

That's much nicer now, isn't it?

A Small Enhancement

Still, it's a bit inconvenient that we have to use the rec_ member to get to the OSVERSIONINFO fields. We can implement operator() to make this a little nicer:

OSVERSIONINFO& operator()
{
    return rec_;
}

This makes the following statements equivalent:

osvi.rec_.dwMajorVersion = 5;
osvi().dwMajorVersion = 5;  // same thing

It's a little strange to use a data object as a function, but to me it seems a worthwhile tradeoff to not have to remember the name of the rec_ member. Since the internal structure is public, either way works.

It would be even better if we could override the . (dot) operator. But, alas, that's not legal C++.

Generalizing with a Template

So now we've got a well-behaved class that makes for cleaner code by getting rid of the extra initialization steps. But Windows is full of such structures -- do we have to write a new class for each one?

Of course not. Let's templatize our class so that it can be used in conjunction with any data structure that needs a count in it's first DWORD member. Our final result is this:

template<class T>
class SizedStruct
{
public:
    SizedStruct()
    {
        ::ZeroMemory(&rec_, sizeof(rec_));
        *reinterpret_cast<DWORD*>(&rec_) = sizeof(rec_);
    }

    T* operator&
    {
        return &rec_;
    }

    T& operator()
    {
        return rec_;
    }

    T rec_;
};

If you're unfamiliar with templates, that might look confusing. Think of them as super macros, where the "T"s in the body of the class will be replaced (at compile time) with the structure name that you used when declaring an object of the template class type. See the examples below, and compare the template class with the type-specific code.

Note that the constructor gets a bit more complicated here because each structure has a different name for its initial size count. Since we can't assume the member name, we have to use casting, treating the pointer to the structure itself as a pointer to the first DWORD field.

Usage Examples

We use the template class like this:

SizedStruct<OSVERSIONINFO> osvi;
if (::GetOSVersionInfo(&osvi) == 0)
    // ...

SizedStruct<DISPLAY_DEVICE> dd;
if (EnumDisplayDevices(NULL, 0, &dd, 0) != 0)
   // ...

SizedStruct<DDSURFACEDESC> ddsd;
ddsd().dwFlags = DDSD_BACKBUFFERCOUNT;
ddsd().dwBackBufferCount = 1;
CreateSurface(&ddsd, &surfacePtr, NULL);

Doing things the C way here would have added six lines of code, about doubling the source size. Isn't it nice not to have to read that? Saves typing, too.

That's It

All of the above has no mention of polymorphism, object orientation, virtual functions -- it's really just "C++ as a better C". But, as the name implies, it's better than straight C. I hope this will spur you to write little C++ helper classes rather than continuing to use C-style programming.

The poor guy that maintains the code that you're writing will thank you.

Dave Townsend / townsend@patriot.net / 08 Mar 05

Home