Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C/C++ macros programming

0.00/5 (No votes)
24 Apr 2008 2  
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>
void ZeroInit(T& val) { val = 0; }
template <>
void ZeroInit<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 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