Click here to Skip to main content
Click here to Skip to main content

Tagged as

C/C++ macros programming

, 24 Apr 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
Sophisticated use of macros, never write things twice!

Introduction

All the C/C++ textbooks I've ever read criticize the use of macros. "Don't use them, they're dangerous because they conceal what you actually write. Especially function-looking macros." Some even say there's no justification of using macros with the invention of C++ with its template classes.

Nevertheless, macros are still used in some places.

For example, debug macros such as ASSERT, VERIFY, TRACE etc. They're all function-looking macros which expand into different things under debug and release builds.

I know some people who never use those macros. Instead, they just use functions that are implemented differently in debug and release builds. Why? Because they're afraid of the threat of using function-looking macros. That is, they prefer that the expression inside ASSERT is always evaluated, even in release build where its value is not used. Well, this may be justified for some. Indeed, placing some important code inside ASSERT (rather than VERIFY) is a pretty common error.

I personally always use macro-versions of those. That's because I use debug macros very heavily to get immediate indication when something goes wrong, whereas on the other side, I don't want the final executable to come with all this crap.

Another example of widely used macros is related to ANSI/Unicode. This includes _T(), all the _tprintf-like macros. Also, Windows API has many functions with either the A or W suffix, such as GetMessageA, GetMessageW. That is, GetMessage is actually a macro, which expands into one of those depending on the build settings.

Despite the criticism, macros are still being used. One may argue if this is justified or not, but this is not the subject of the article. I want to show in this article really amazing things that can be done via the sophisticated use of macros. Whether to use those techniques or not - the decision is up to you.

Communication protocol example

Suppose you have to implement some communication protocol. This protocol consists of 'messages' of different kinds, and every message has its specific parameters.

Let's agree (for now) that every transferred message starts with its 4-byte size (thus restricting the largest message to the order of 4GB), then comes its 2-byte code, and then come all its parameters which are message-dependent. Ordinal types (ULONG, UCHAR, USHORT, double, ...) are transferred as-is (without Big/Little Endian conversion). Strings are transferred in the Unicode character set (UTF16), with a preceding ULONG that specifies the length of the string in characters.

To begin with, we want the following message types:

  1. Login. Contains the client version (ULONG), Username (string), Password (string).
  2. Login result. Contains the login result code (UCHAR). 0=OK, 1=invalid credentials, etc.
  3. Chat message. Contains the recipient username (string), chat text (string), some extra codes (ULONG).

So, how do we implement this? For every message type, we need the following:

  • Declaration of the message class/struct.
  • Code that writes this message into a stream (socket/file/etc.).
  • Code that parses messages from the stream.
  • Code that handles incoming messages.

For the purposes of this example, we'll use the following abstract classes for streaming:

struct OutStream {
    virtual void Write(LPCVOID pPtr, size_t nSize) = 0;

    // ordinal types
    template <class T>
    void Write_T(const T& val)
    {
        Write(&val, sizeof(val));
    }
    // variable-sized strings
    void Write_Str(const CString& val)
    {
        ULONG nLen = val.GetLength();
        Write_T(nLen);
        Write((PCWSTR) val, nLen * sizeof(WCHAR));
    }
};
struct InStream {
    virtual size_t Read(LPVOID pPtr, size_t nSize) = 0;

    bool ReadExactTry(LPVOID pPtr, size_t nSize)
    {
        while (true)
        {
            size_t nPortion = Read(pPtr, nSize);
            if (nPortion == nSize)
                return true; // ok
            if (!nPortion)
                return false; // not enough data.
            nSize -= nPortion;
            if (pPtr)
                ((PBYTE&) pPtr) += nPortion;
        }
    }
    void ReadExact(LPVOID pPtr, size_t nSize)
    {
        if (!ReadExactTry(pPtr, nSize))
        {
            // not enough data, raise an appropriate exception
            throw _T("not enough data!");
        }
    }

    // ordinal types
    template <class T>
    void ReadExact_T(T& val)
    {
        ReadExact(&val, sizeof(val));
    }
    // variable-sized strings
    void ReadExact_Str(CString& val)
    {
        ULONG nLen;
        ReadExact_T(nLen);
        PWSTR szPtr = val.GetBuffer(nLen);
        ReadExact(szPtr, nLen * sizeof(WCHAR));
        val.ReleaseBuffer(nLen);
    }
};

Now, let's implement our messages. For example, the login message may be declared in the following manner:

struct MsgLogin
{
    // message fields
    ULONG m_Version;
    CString m_Username;
    CString m_Password;

    MsgLogin()
    {
        // zero-init members.
        m_Version = 0;
    }

    void Write(OutStream&);
    void Read(InStream&);
};


void MsgLogin::Write(OutStream& out)
{
    // first comes the message size (in bytes). Let's calculate it.
    ULONG nSize = 
        sizeof(ULONG) +        // message size
        sizeof(USHORT) +    // message code
        sizeof(m_Version) +
        sizeof(ULONG) + m_Username.GetLength() * sizeof(WCHAR) +
        sizeof(ULONG) + m_Password.GetLength() * sizeof(WCHAR);

    out.Write_T(nSize);
    out.Write_T((USHORT) 1); // the code of the login.
    out.Write_T(m_Version);
    out.Write_Str(m_Username);
    out.Write_Str(m_Password);
}
void MsgLogin::Read(InStream& in)
{
    in.ReadExact_T(m_Version);
    in.ReadExact_Str(m_Username);
    in.ReadExact_Str(m_Password);
}

Then, in order to send/save this message, you'll have to write it this way:

MsgLogin login;
login.m_Version = MAKELONG(1, 3);
login.m_Username = _T("user");
login.m_Password = _T("pass");
login.Write(my_out_stream);

The message parsing code should be something like this:

while (true)
{
    ULONG nSize;
    if (!my_in_stream.ReadExactTry(&nSize, sizeof(nSize)))
        break; // no more messages so far.
    
    USHORT nCode;
    my_in_stream.ReadExact_T(nCode);
    switch (nCode)
    {
    case 1: // login
        {
            MsgLogin login;
            login.Read(my_in_stream);
            // process incoming message
            HandleMsg(login);
        }
        break;
    default:
        // unknown message, bypass it.
        for (nSize -= sizeof(ULONG) + sizeof(USHORT); nSize; )
        {
            BYTE pBuf[0x100];
            size_t nPortion = min(sizeof(pBuf), nSize);
            my_in_stream.ReadExact(pBuf, nPortion);
            nSize -= (ULONG) nPortion;
        }
    }
}

Now, we have to add other messages. For each of them, we'll write their struct declaration, the Write method, the Read method, and extend the parser switch. Furthermore, every field of every message must be taken into account in several places: twice in the Write method (when calculating the size and actually storing), in the Read method, and in the constructor (should be zero-initialized).

Writing all this is a pretty large routine work, with reasonable chance of typing/copy-paste mistakes. Now, let's demonstrate how this can be done via macros.

First, we declare the list of messages we want to have:

#define COMM_MSGS_All \
    COMM_MSG(1, Login) \
    COMM_MSG(2, LoginRes) \
    COMM_MSG(3, Chat)

What does this mean? By now, it actually means nothing. The macro COMM_MSGS_All just expands into a list of COMM_MSG with two parameters: the message code and its name. COMM_MSG is not yet defined, so that COMM_MSGS_All can't be used meanwhile.

Now, let's write for every message the members we want in it:

#define COMM_MSG_Login(par) \
    par(ULONG, Version) \
    par(CString, Username) \
    par(CString, Password)

#define COMM_MSG_LoginRes(par) \
    par(UCHAR, Result)

#define COMM_MSG_Chat(par) \
    par(CString, Recipient) \
    par(CString, Test) \
    par(UCHAR, Flags)

Again, those three macros we've just defined don't mean too much. They just list in an abstract (not yet defined) way what our messages should contain. Note: Each of these macros needs a parameter (par) via which they list the message members.

And now, let's breath life into our macros. First, we said we need to declare a struct for every message type. Let's do it:

#define PAR_DECL(type, name) type m_##name;
#define PAR_ZERO(type, name) ZeroInit(m_##name);

template <class T><class />
void ZeroInit(T& val) { val = 0; }
template <>
void ZeroInit<CString><cstring />(CString& val) { }

#define COMM_MSG(code, name) \
struct Msg##name { \
    COMM_MSG_##name(PAR_DECL) \
    Msg##name() \
    { \
        COMM_MSG_##name(PAR_ZERO) \
    } \
    void Write(OutStream&); \
    void Read(InStream&); \
};

COMM_MSGS_All
#undef COMM_MSG

What does this mean? Let's see. As we said, COMM_MSGS_All expands into a list of COMM_MSG per message. COMM_MSG in turn takes two parameters: the message code and its name. We defined COMM_MSG (code, name) to expand into a declaration of a Msg##name struct (## means concatenation of tokens). So, COMM_MSGS_All in fact expands into a declaration of a struct per message.

The first line inside the struct declaration is COMM_MSG_##name(PAR_DECL). It means that for every message type, COMM_MSG_##name will turn into an appropriate message's parameter list. This list requires a parameter which we specified: PAR_DECL. Every message field is interpreted via this macro. Our PAR_DECL macro expands into type m_##name;. So, for every field of the current message, we declare a member in our struct of the appropriate type and name (with a m_ prefix).

Next, we have a constructor. It has a COMM_MSG_##name(PAR_ZERO) statement, which passes all the message's fields via the PAR_ZERO macro. This macro calls the ZeroInit function for every member. This is a template function that should zero-init the member. For most of the types, it just assigns 0 to it. The exception is CString (template function specialization). CString doesn't require explicit zero-init.

Next, we declare Write and Read methods inside our struct. Let's now implement them:

template<class T>
ULONG CalcSizeOf(const T& val) { return sizeof(val); }
template <>
ULONG CalcSizeOf<CString><cstring>(const CString& val)
{
    return
        sizeof(ULONG) +
        val.GetLength() * sizeof(WCHAR);
}

template <class T><class>
void WriteToStream(OutStream& out, const T& val) { out.Write_T(val); }
template <>
void WriteToStream<CString><cstring>(OutStream& out, const CString& val)
{
    out.Write_Str(val);
}
template <class T><class>
void ReadFromStream(InStream& in, T& val) { in.ReadExact_T(val); }
template <>
void ReadFromStream<CString><cstring>(InStream& in, CString& val)
{
    in.ReadExact_Str(val);
}

#define PAR_CALCSIZE(type, name) +CalcSizeOf(m_##name)
#define PAR_WRITE(type, name) WriteToStream(out, m_##name);
#define PAR_READ(type, name) ReadFromStream(in, m_##name);

#define COMM_MSG(code, name) \
void Msg##name::Read(InStream& in) \
{ \
    COMM_MSG_##name(PAR_READ); \
} \
void Msg##name::Write(OutStream& out) \
{ \
    ULONG nSize = \
        sizeof(ULONG) + sizeof(USHORT) \
        COMM_MSG_##name(PAR_CALCSIZE); \
    out.Write_T(nSize); \
    out.Write_T((USHORT) code); \
    COMM_MSG_##name(PAR_WRITE) \
}

COMM_MSGS_All
#undef COMM_MSG

We again use COMM_MSGS_All, but this time, we define COMM_MSG to be something else (note that after every usage of COMM_MSGS_All, we immediately undefine the COMM_MSG). This time, we make it turn into Read and Write methods for the specified message.

Inside the Read method, we pass all our members through the PAR_READ macro, which turns into ReadFromStream(in, m_##name). This is a template function that is implemented differently for CString and ordinal types. The Write function makes use of the parameters list twice: once for calculating the message size, and once for actual writing. For this purpose, we prepared the PAR_CALCSIZE and PAR_WRITE macros, which in turn call the relevant template functions that behave differently for ordinal types and CString.

Now, the parser code turns into the following:

while (true)
{
    ULONG nSize;
    if (!my_in_stream.ReadExactTry(&nSize, sizeof(nSize)))
        break; // no more messages so far.
    
    USHORT nCode;
    my_in_stream.ReadExact_T(nCode);
    switch (nCode)
    {
#define COMM_MSG(code, name) case code: \
        { \
            Msg##name msg; \
            msg.Read(my_in_stream); \
            HandleMsg(msg); \
        } \
        break;
COMM_MSGS_All
#undef COMM_MSG

    default:
        // unknown message, bypass it.
        for (nSize -= sizeof(ULONG) + sizeof(USHORT); nSize; )
        {
            BYTE pBuf[0x100];
            size_t nPortion = min(sizeof(pBuf), nSize);
            my_in_stream.ReadExact(pBuf, nPortion);
            nSize -= (ULONG) nPortion;
        }
    }
}

That is, for every known message, we parse it and call the overloaded HandleMsg function. What still remains is implementing the appropriate HandleMsg functions for every message type. But, this of course depends on the program logic. All other things are implemented automatically via our macros.

Let's make some conclusions.

What exactly did we achieve, apart of making the program absolutely unreadable?

The answer is that we made the generation of structs for messages, their serialization, and parsing automatic. If you want to add another field to a message, there is only a *single* place you have to change: the appropriate COMM_MSG_xxxx macro.

If you add a new message, then you'll have to list its parameters and append its entry into the COMM_MSGS_All macro. Plus, you'll have to write a handling function for it. And, this is all!

Compare this with what we had at the beginning: for every message type, you write all the methods. When you have tens of different message types, it's a nightmare!

Now, suppose we decided to change the protocol. For instance, we don't want to place the message size into the stream (thus making bypassing unknown messages impossible). You have tens of places to fix!!! And in our case, we have only *one* place to fix.

Let's go even further: for every message, we want a run-time textual description of its members, which we can log/display. Let's implement it:

#define PAR_FMT_UCHAR "u"
#define PAR_FMT_USHORT "u"
#define PAR_FMT_ULONG "u"
#define PAR_FMT_CString "s"
#define PAR_FMT1(type, name) _T("\t") _T(#name) 
  _T(" = %") _T(PAR_FMT_##type) _T("\r\n")
#define PAR_FMT2(type, name) ,msg.m_##name

#define COMM_MSG(code, name) \
void TxtDescr(const Msg##name& msg, CString& sOut) \
{ \
    sOut.Format(_T("Type=%s, Code=%u\r\n") \
        COMM_MSG_##name(PAR_FMT1) \
        ,_T(#name), code \
        COMM_MSG_##name(PAR_FMT2)); \
}

COMM_MSGS_All
#undef COMM_MSG

We generate TxtDescr for every message type, and we use CString's Format function. We pass through our parameters twice: the first time when we build the format string, the second time when we pass the appropriate parameter. When we build the format string for every parameter, we use PAR_FMT_##type which expands to PAR_FMT_USHORT, PAR_FMT_CString etc. Thus, for every such a thing, we need to define the correct formatting flag.

And, this is automatically done for all the messages.

You don't like this implementation? Then rewrite it. There's absolutely no problem, because you only have a *single* place to rewrite.

Impressive, isn't it? So, let's think again if it is worth to use macros.

Are they dangerous? Of course. You change one character in a macro definition - and the whole code for all the messages may become totally different.

But, what is the alternative? Writing, writing, copy+pasting and etc.? From my personal experience, rewriting the same thing multiple times, besides of demoralization, is much more dangerous than using macros.

If you write something wrong in the macro, most probably it just won't compile. And even if it will, it will work wrong probably for all kinds of messages. And if something works wrong, there's only a *single* place that you have to fix.

And if you just manually write all the functions for all the message types, what are the chances to do it without mistakes? Pretty close to zero, unless you're a robot. And, if you make a mistake in one message which is rarely used, you'll not know it until you get the surprise.

Yes, macros are very dangerous, they require good skills to write, it's very hard to read them, and it's impossible to debug them. But, they eliminate the need to rewrite the same thing several times.

This may sound crazy, but I think it's easier to maintain macros than dozens of almost identical code lines. You need to change something - go ahead, change just *one* specific place.

Is there another way to implement messages without rewriting too many things and not using macros? In this specific example, yes. We could write it this way:

struct ParamBase {
    PCTSTR m_szName;
    virtual void Write(OutStream&) = 0;
    virtual void Read(InStream&) = 0;
    virtual ULONG CalcSize() = 0;
};
struct Param_UCHAR :public ParamBase {
    UCHAR m_Val;
    virtual void Write(OutStream&);
    virtual void Read(InStream&);
    virtual ULONG CalcSize();
};
// ...

struct MsgBase {
    std::list<parambase*> m_lstParams;
};
struct MsgLogin :public MsgBase {
    MsgLogin()
    {
        m_lstParams.push_tail(new ParamULong(_T("Version")));
        m_lstParams.push_tail(new ParamString(_T("Username")));
        m_lstParams.push_tail(new ParamString(_T("Password")));
    }
};
// ...

That is, we arrange all our parameters in a list, which can be used for message serialization. But, I don't like this method. It means that instantiation, serialization, parsing, etc. is done at run-time instead of compile-time, plus it demands dynamic memory allocations; hence, we have a performance hit. And anyway, it doesn't allow us to get rid of rewriting things. What if at some point we decide to replace an STL list by another list implementation? Then, we need to fix tens of places!

Macros on the other side give you the maximum flexibility.

Callback functions example

Once I had to write a video wrapper (filter-like) driver. At initialization, the video driver is requested to fill a table with its supported callback functions. My driver then had to call the initialization function of the original driver and get its functions. Some of them had to be filtered out, some replaced by mine, some taken as-is.

At some point, I decided to use SEH (structured exception handling) in my filter functions; means - every such top-level function has to be wrapped with the SEH handler. Because I was already using macros to manipulate my functions, it took only a minute for me to add the SEH handler for all my functions!

By the way, instead of listing all the functions by one single macro (like COMM_MSGS_All in the previous example), this time I had several macros for listing functions of a specific category: those that must always be hooked, those that must be replaced, and so on.

I'll not list the code here, it's too complicated and requires a lot of explanation. But, you can believe me, it was a pleasure. Instead of messing with tens of thousands of code lines of repetitions, I only had to write *one* elegant macro that does it all.

Note also that in this case, there's no alternative with either C++ polymorphic stuff (run-time parameters list etc.) or template functions.

Callback marshalling

Marshalling - I mean I have callback functions that are called in one thread. I want them to post a message to another thread and return immediately. Then, in the other thread, I want an appropriate callback to be called with all the needed parameters.

Without getting into too much details, this is how I implemented it:

#define HANDLE_MY_EVTS \
    MARSHAL_IN(OnJoinSession) \
    MARSHAL_IN(OnClientUp) \
//...


#define MARSHAL_PARAMS_OnJoinSession(macro, sep) \
        macro(ULONG, ULONG, nUserSeq) \
        sep \
        macro(UCHAR, UCHAR, nState)
        sep \
        macro(PCTSTR, CString, sClientName) \
        sep \
        macro(const SYSTEMTIME&, SYSTEMTIME, tmOnline)


#define MARSHAL_PARAMS_OnClientUp(macro, sep) \
        macro(ULONG, ULONG, nUserSeq) \
        sep \
        macro(ULONG, ULONG, nRemoteID)

// ...

Note: Unlike the previous example, parameters list now has two types for every parameter: first is the type our callback function should receive according to the agreed prototype. And, the second is the type of the variable that can be used to marshal this parameter. For ordinal types (ULONG, UCHAR, ...), it's the same. But for things such as PCTSTR, it's different: you can't just take the pointer, you need to create a copy of the string.

Let's declare the functions that we want to be called:

#define THE_MACRO(type1, type2, val) type1 val
#define THE_SEP ,
#define MARSHAL_IN(method) \
    void method(MARSHAL_PARAMS_##method(THE_MACRO, THE_SEP));
    HANDLE_MY_EVTS
#undef MARSHAL_IN
#undef THE_MACRO
#undef THE_SEP

Another difference: after every parameter, there goes sep (also passed via a macro parameter). Due to this thing, we can separate the parameters by comma in the above function declaration.

Now, whenever our callback function is called, we allocate a task structure with the needed parameters and post it to our dedicated thread. Let's write it:

#define THE_MACRO(type1, type2, val) type2 m_##val;
#define THE_SEP
#define MARSHAL_IN(method) \
struct CTaskGui_##method : public CTaskGui { \
    virtual ~CTaskGui_##method() {}\
    MARSHAL_PARAMS_##method(THE_MACRO, THE_SEP) \
    virtual void ExecuteUIGuarded(); \
};
HANDLE_MY_EVTS
#undef MARSHAL_IN
#undef THE_MACRO
#undef THE_SEP


// Callback functions:

#define THE_MACRO(type1, type2, val) type1 val
#define THE_SEP ,
#define ASSIGN_MACRO(type1, type2, val) spTask->m_##val = val;
#define ASSIGN_SEP
#define PASS_MACRO(type1, type2, val) m_##val
#define MARSHAL_IN(method) \
void THE_EVT::method(MARSHAL_PARAMS_##method(THE_MACRO, THE_SEP)) \
{ \
    if (IsEventAllowed<evt_##method>()) \
    { \
        CComSPtrBase<ctaskgui_##method> spTask = CTaskGui_##method::AllocOnHeap(); \
        MARSHAL_PARAMS_##method(ASSIGN_MACRO, ASSIGN_SEP) \
        m_spUIMarshaller->PostGui(spTask); \
    } \
} \
void CTaskGui_##method::ExecuteUIGuarded() \
{ \
    method(MARSHAL_PARAMS_##method(PASS_MACRO, THE_SEP)); \
}

and so on.

Some other examples

I've seen tons of code written this way:

if (sfState.Flag & HOOK_CreatePort)
{
    if (sfHList.pfnCreatePort != NULL)
    {
        sfHList.pfnOrgCreatePort = pGlobalProc[IDX_CreatePort];
        pGlobalProc[IDX_CreatePort] = sfHList.pfnCreatePort;

    }
    hooks_total++;
}
if (sfState.Flag & HOOK_CreateWaitablePort)
{
    if (sfHList.pfnCreateWaitablePort != NULL)
    {
        sfHList.pfnOrgCreateWaitablePort = pGlobalProc[IDX_CreateWaitablePort];
        pGlobalProc[IDX_CreateWaitablePort] = sfHList.pfnCreateWaitablePort;

    }
    hooks_total++;
}
if (sfState.Flag & HOOK_ConnectPort)
{
    if (sfHList.pfnConnectPort != NULL)
    {
        sfHList.pfnOrgConnectPort = pGlobalProc[IDX_ConnectPort];
        pGlobalProc[IDX_ConnectPort] = sfHList.pfnConnectPort;

    }
    hooks_total++;
}
// ... and tens more ...

Using macros, we can turn it into:

#define HOOK_FUNC(func) \
if (sfState.Flag & HOOK_##func) \
{ \
    if (sfHList.pfn##func != NULL) \
    { \
        sfHList.pfnOrg##func = pGlobalProc[IDX_##func]; \
        pGlobalProc[IDX_##func] = sfHList.pfn##func; \
    } \
    hooks_total++; \
}
HOOK_FUNC(CreatePort)
HOOK_FUNC(CreateWaitablePort)
HOOK_FUNC(ConnectPort)
// ...

When you write MFC dialog-based applications, you can put/get values of your controls via the so-called DDX mechanism. For every such control, you can declare a variable via wizard, and the wizard will automatically add it to the DoDataExchange function and zero-initializa it in the constructor.

But many times, I also needed something that the wizard didn't implement automatically: store those parameters in the Registry and read them back. Macros helped me again.

Once I had to construct/parse an XML (which I hate), and it demanded 'special' characters encodings. I declared those 'special' characters encoding rules:

#define XML_SPECIAL_BRACKETS \
    XML_SPECIAL_CHAR('>',  "gt") \
    XML_SPECIAL_CHAR('<',  "lt")

#define XML_SPECIAL_QUOTES \
    XML_SPECIAL_CHAR('\"', "quot") \
    XML_SPECIAL_CHAR('\'', "apos")

#define XML_SPECIAL_AMP \
    XML_SPECIAL_CHAR('&',  "amp")

#define XML_SPECIAL_BRACKETS_AMP \
    XML_SPECIAL_BRACKETS \
    XML_SPECIAL_AMP

#define XML_SPECIAL_BRACKETS_AMP_LEND \
    XML_SPECIAL_BRACKETS_AMP \
    XML_SPECIAL_CHAR('\r', "#xA") \
    XML_SPECIAL_CHAR('\n', "#xD")

They had to be separated into categories, because there are slightly different rules for encoding characters for attributes and regular XML nodes.

Now, whenever I need to encode/decode XML strings, I just declare XML_SPECIAL_CHAR to do the appropriate thing, and list all the special characters that are relevant for the case.

Another great thing that I managed to do via macros: we had a Direct3D application that does a lot of drawing. Once I needed to record all the drawings without affecting the performance too much. By using macros, I wrapped all the Direct3D functions with the code that, apart of actually drawing, records (by the way, similar to the messages example) all the function calls with all the relevant parameters. Then, by using the macros again, this stream can be parsed and all the functions 'replayed'.

Conclusions

Sometimes, template functions/classes may be used instead. If so, of course, they're the preferred way to go. But usually, the situation is more cruel.

By such a use of macros, I have almost no repetitions in my code. In my opinion, repetition is the worst thing in programming. I'm aware of all the threats with using macros, and I still prefer macros to infinite code rewrites. Yes, macros are like puzzles, sometimes not trivial to understand. But they make the program very compact.

I also prefer using macros to re-arrange code in a super-polymorphic way in order to minimize rewrites, but this is my opinion, I don't insist. All this is the matter of opinion after all.

Whereas using of macros is criticized in all the books and articles I've read till now, I pointed out some definite advantages of them. Is it worth using them - the decision is up to you.

Comments are welcome, as usual. Both positive and negative.

License

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

Share

About the Author

valdok
Software Developer (Senior)
Israel Israel
My name is Vladislav Gelfer, I was born in Kiev (former Soviet Union), since 1993 I live in Israel.
In programming I'm interested mostly in low-level, OOP design, DSP and multimedia.
Besides of the programming I like physics, math, digital photography.

Comments and Discussions

 
GeneralMacros the necessary evil Pinmemberserup14-Mar-11 9:43 
GeneralRe Pinmembervaldok14-Mar-11 11:34 
GeneralGood example PinmemberRichard MacCutchan27-Nov-09 23:30 
GeneralRe Pinmembervaldok27-Nov-09 23:54 
GeneralExactly right! PinmemberDQNOK29-Apr-08 10:45 
GeneralRe: Exactly right! PinmemberBob100029-Apr-08 23:56 
GeneralFlawed line of argumentation PinmemberStefan6329-Apr-08 6:46 
GeneralRe: Flawed line of argumentation Pinmembervaldok29-Apr-08 20:56 
a) I don't insist my opinion is better than someone else's. I just want to *express* my opinion. Is it better ? Just let it speak for itself.
 
b) I don't want to argue about MFC code here. But about old macros - they're still widely used. IMHO - there's no problem with them.
 
c) About replacing macros with something else - I think you've read the article inattentionally. I
specifically pointed out that **in this specific example** you could use C++ polimorphism in order to implement them, *but*
1. You *must* change the program flow. That is, many thing that could be done in compile-time would work in run-time now, whereas macros give you more flexibility.
2. Anyway, you will *have to* write a lot of almost identical code lines. Considerably less than without using anything at all, but still something. And if you eventually have to fix something - you'll have to fix *many* places. Whereas with macros you do everything in *ONE* place. I've even demonstrated - using stl list as a dynamic container for parameters, eventually you may want to replace it with something else.
3. Not always possible to get rid of repititions (at least partially) with the use of
polimorphism/template stuff. Look at the example with callback functions. Well, I didn't published
it, it's too complicated. But you can figure it out. If you can show me how to generate callback
functions (which you can access via function pointers) via C++ stuff - I will be glad to see.
 
About the variable names - Sometimes it's good to have it in release too. Once using macros I've
made a simulator program, with UI (including buttons, edit boxes, combo boxes and etc.) through which you may select a message of some type, fill its parameters and send it. And guess how many repititions I had in the code? Right, *none*.
 
d) It has absolutely *nothing* with self-modifying code. All the plays with macros affect the
compilation process. After you've built the executable - there's no more macros. I think you confuse between compile-time and run-time.
 

At 20125 a random fellow programmer stares at an old macro COMM_MSG, which is defined to be... But wait, it's undefined!
 
Look carefully, each time this macro is defined only just before comes COMM_MSGS_All, and it is immediately undefined after that!
 
This is the point! Which you (and probably many others) miss competely.
 

The *only* thing which is defined, and *never* redefined, is COMM_MSGS_All (and per-message parameters lists). It has *all* the comprehensive information about the message, and it has *nothing* more than this! And there's only *one* such a declaration.
 
In order to turn it into something specific you define COMM_MSG to something you want, use the COMM_MSGS_All, and immediately undefine COMM_MSG!
 
In such a way you *never* mention message declarations directly. You need more messages or want to change their fields - then you fix this *single* place. And the whole applications (and sometimes even several applications, like my simulator) will be generated according to new messages.
 
I'll go even further: at 20125 most of the modern code will be completely rewritten, because of the
new machine types, another demands, maybe it will be for different OS, brand new applications, and etc.
But what will still be there is - guess what? Yes, the COMM_MSGS_All macro. It will still contain the list of all the messages (probably much bigger now) in exactly the same way.
 
"This, exactly, is the kind of macro programming all the books you mentioned at the start are trying to warn you against!"
 
Then I've succeeded in my goal - show some definite advantages of using macros, specifically in this "bad" way.
 
I really advise you to take the example with the messages, make it compile, and then add 10-20
more. You'll see how many code lines you'll have to write, even with C++ polimorphism.
Vladik.

GeneralRe: Flawed line of argumentation Pinmemberserup14-Mar-11 10:13 
GeneralYes - but think it does prove Macros can be difficult to follow! PinmemberBob100028-Apr-08 14:57 
GeneralRe Pinmembervaldok29-Apr-08 21:08 
GeneralRe: Re PinmemberBob100029-Apr-08 23:47 
GeneralNice article, but your examples are not so much persuasive PinmemberHuyong Zhao24-Apr-08 19:47 
GeneralOh yes they are! Pinmembervaldok26-Apr-08 21:49 

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

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

| Advertise | Privacy | Mobile
Web02 | 2.8.141022.2 | Last Updated 24 Apr 2008
Article Copyright 2008 by valdok
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid