C/C++ 宏编程
精通宏的使用,告别重复编写!
引言
我读过的所有 C/C++ 教科书都批评宏的使用。“不要使用它们,它们很危险,因为它们隐藏了你实际编写的代码。特别是看起来像函数的宏。” 有些人甚至说,有了 C++ 的模板类,使用宏根本没有理由了。
然而,宏仍然在某些地方被使用。
例如,调试宏,如 ASSERT
、VERIFY
、TRACE
等。它们都是看起来像函数的宏,在调试和发布版本中会扩展成不同的东西。
我知道有些人从不使用这些宏。相反,他们只使用在调试和发布版本中实现方式不同的函数。为什么?因为他们害怕使用看起来像函数的宏的威胁。也就是说,他们宁愿 ASSERT
中的表达式总是被评估,即使在发布版本中它的值没有被使用。嗯,这可能对某些人来说是合理的。事实上,将一些重要代码放在 ASSERT
(而不是 VERIFY
)中是一个相当常见的错误。
我个人总是使用这些宏的版本。这是因为我非常依赖调试宏来在出现问题时立即获得指示,而在另一方面,我不希望最终的可执行文件包含所有这些垃圾。
另一个广泛使用的宏的例子与 ANSI/Unicode 相关。这包括 _T
(),所有 _tprintf
类似的宏。此外,Windows API 有许多带有 A
或 W
后缀的函数,例如 GetMessageA
、GetMessageW
。也就是说,GetMessage
实际上是一个宏,它会根据构建设置扩展成其中一个。
尽管受到批评,宏仍然在使用。有人可能会争论这是否合理,但这并不是本文的主题。我想在这篇文章中展示通过精通宏的使用可以实现多么惊人的事情。是否使用这些技术——决定权在你。
通信协议示例
假设您必须实现某种通信协议。该协议由不同类型的“消息”组成,每种消息都有其特定的参数。
让我们同意(目前)每条传输的消息都以其 4 字节大小开始(因此限制最大消息到 4GB 级别),然后是 2 字节的代码,然后是所有消息依赖的参数。序数类型(ULONG
、UCHAR
、USHORT
、double
等)按原样传输(没有 Big/Little Endian 转换)。字符串以 Unicode 字符集(UTF16)传输,前面有一个 ULONG
,指定字符串的字符长度。
首先,我们希望以下消息类型
- 登录。包含客户端版本(
ULONG
)、用户名(字符串)、密码(字符串)。 - 登录结果。包含登录结果代码(
UCHAR
)。0=成功,1=无效凭据,依此类推。 - 聊天消息。包含接收者用户名(字符串)、聊天文本(字符串)、一些额外代码(
ULONG
)。
那么,我们如何实现这一点呢?对于每种消息类型,我们需要以下内容
- 消息类/结构的声明。
- 将此消息写入流(套接字/文件/等)的代码。
- 从流中解析消息的代码。
- 处理传入消息的代码。
为了这个例子,我们将使用以下抽象类进行流式处理
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);
}
};
现在,让我们实现我们的消息。例如,登录消息可以这样声明
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);
}
然后,为了发送/保存此消息,您需要这样编写
MsgLogin login;
login.m_Version = MAKELONG(1, 3);
login.m_Username = _T("user");
login.m_Password = _T("pass");
login.Write(my_out_stream);
消息解析代码应该类似这样
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;
}
}
}
现在,我们必须添加其他消息。对于其中的每一个,我们将编写它们的结构声明、Write
方法、Read
方法,并扩展解析器 switch
。此外,每条消息的每个字段都必须在几个地方被考虑:在 Write
方法中两次(计算大小和实际存储时),在 Read
方法中,以及在构造函数中(应零初始化)。
编写所有这些是一个相当繁琐的工作,有合理的输入/复制粘贴错误的几率。现在,让我们演示如何通过宏来完成。
首先,我们声明我们想要的消息列表
#define COMM_MSGS_All \
COMM_MSG(1, Login) \
COMM_MSG(2, LoginRes) \
COMM_MSG(3, Chat)
这意味着什么?到目前为止,它实际上什么也没有。宏 COMM_MSGS_All
仅扩展为具有两个参数的 COMM_MSG
列表:消息代码及其名称。COMM_MSG
尚未定义,因此 COMM_MSGS_All
目前无法使用。
现在,让我们为每条消息编写我们想要包含的成员
#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)
同样,我们刚刚定义的这三个宏并没有太大意义。它们只是以抽象的(尚未定义)方式列出了我们的消息应该包含的内容。注意:这些宏中的每一个都需要一个参数(par
),通过该参数列出消息成员。
现在,让我们赋予我们的宏生命。首先,我们说我们需要为每种消息类型声明一个结构。让我们这样做
#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
这意味着什么?让我们看看。正如我们所说,COMM_MSGS_All
扩展为每条消息的 COMM_MSG
列表。COMM_MSG
反过来接受两个参数:消息代码和名称。我们将 COMM_MSG
(代码,名称)定义为扩展为 Msg##name
结构(## 表示令牌连接)的声明。因此,COMM_MSGS_All
实际上扩展为每条消息的结构声明。
结构声明中的第一行是 COMM_MSG_##name(PAR_DECL)
。这意味着对于每种消息类型,COMM_MSG_##name
将变成一个适当的消息参数列表。这个列表需要一个我们指定的参数:PAR_DECL
。每条消息字段都通过此宏进行解释。我们的 PAR_DECL
宏扩展为 type m_##name;
。因此,对于当前消息的每个字段,我们在结构中声明一个适当类型和名称(带有 m_
前缀)的成员。
接下来,我们有一个构造函数。它有一个 COMM_MSG_##name
(PAR_ZERO
) 语句,它通过 PAR_ZERO
宏传递所有消息字段。此宏为每个成员调用 ZeroInit
函数。这是一个模板函数,它会为成员进行零初始化。对于大多数类型,它只是将其赋值为 0。唯一的例外是 CString
(模板函数特化)。CString
不需要显式零初始化。
接下来,我们在结构中声明 Write
和 Read
方法。让我们现在来实现它们
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
我们再次使用 COMM_MSGS_All
,但这次,我们将 COMM_MSG
定义为其他东西(注意,在每次使用 COMM_MSGS_All
之后,我们立即取消定义 COMM_MSG
)。这次,我们让它变成指定消息的 Read
和 Write
方法。
在 Read
方法中,我们将所有成员通过 PAR_READ
宏传递,该宏变成 ReadFromStream(in, m_##name)
。这是一个模板函数,它为 CString
和序数类型实现方式不同。Write
函数两次利用参数列表:一次用于计算消息大小,一次用于实际写入。为此,我们准备了 PAR_CALCSIZE
和 PAR_WRITE
宏,它们又调用针对序数类型和 CString
行为不同的相关模板函数。
现在,解析器代码变成了这样
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;
}
}
}
也就是说,对于每个已知消息,我们解析它并调用重载的 HandleMsg
函数。现在剩下的是为每种消息类型实现适当的 HandleMsg
函数。但这当然取决于程序逻辑。所有其他内容都通过我们的宏自动实现。
让我们得出一些结论。
除了让程序完全难以阅读之外,我们到底实现了什么?
答案是我们实现了消息结构、序列化和解析的自动生成。如果您想为消息添加另一个字段,您只需要更改一个单一的地方:相应的 COMM_MSG_
xxxx 宏。
如果您添加了新消息,那么您将需要列出其参数并将其条目附加到 COMM_MSGS_All
宏中。此外,您还需要为其编写一个处理函数。仅此而已!
与我们开始时相比:对于每种消息类型,您编写所有方法。当您有数十种不同的消息类型时,这简直是噩梦!
现在,假设我们决定更改协议。例如,我们不想将消息大小放入流中(因此无法绕过未知消息)。您需要修复数十个地方!!!而在我们的例子中,我们只需要修复一个地方。
让我们更进一步:对于每条消息,我们都想要一个运行时文本描述其成员,我们可以将其记录/显示。让我们实现它
#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
我们为每种消息类型生成 TxtDescr
,并使用 CString
的 Format
函数。我们两次通过参数:第一次构建格式字符串时,第二次传递相应参数时。当为每个参数构建格式字符串时,我们使用 PAR_FMT_##type
,它扩展为 PAR_FMT_USHORT
、PAR_FMT_CString
等。因此,对于这样的每一个东西,我们需要定义正确的格式标志。
而且,这一切都自动为所有消息完成。
您不喜欢这个实现吗?那么请重写它。完全没有问题,因为您只需要重写一个地方。
令人印象深刻,不是吗?那么,让我们再想想是否值得使用宏。
它们危险吗?当然。您更改宏定义中的一个字符——整个代码所有消息都可能变得完全不同。
但是,替代方案是什么?编写、编写、复制粘贴等等?根据我的个人经验,多次重写相同的东西,除了令人沮丧之外,比使用宏危险得多。
如果您在宏中写错了东西,很可能它根本无法编译。即使它能编译,它也可能对所有类型的消息都工作不正常。如果有什么东西工作不正常,您只需要修复一个地方。
如果您只是手动编写所有消息类型的所有函数,那么在没有错误的情况下完成的可能性有多大?几乎为零,除非您是机器人。而且,如果您在一个很少使用的消息中犯了一个错误,直到发生意外之前您都不会知道。
是的,宏非常危险,它们需要熟练的技能才能编写,非常难以阅读,而且无法调试。但是,它们消除了多次重写相同东西的需要。
这可能听起来很疯狂,但我认为维护宏比维护几十行几乎相同的代码更容易。您需要更改某些内容——请继续,只更改一个特定的地方。
有没有其他方法可以实现消息而不重写太多东西并且不使用宏?在这个特定的例子中,可以。我们可以这样写
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")));
}
};
// ...
也就是说,我们将所有参数安排在一个列表中,该列表可用于消息序列化。但我不喜欢这种方法。它意味着实例化、序列化、解析等是在运行时而不是编译时完成的,而且它需要动态内存分配;因此,我们有性能损失。而且,无论如何,它不允许我们摆脱重写东西。如果有一天我们决定将 STL 列表替换为另一个列表实现怎么办?那么,我们需要修复几十个地方!
另一方面,宏为您提供了最大的灵活性。
回调函数示例
我曾经不得不编写一个视频包装器(过滤驱动程序)驱动程序。在初始化时,视频驱动程序被要求用其支持的回调函数填充一个表。然后,我的驱动程序必须调用原始驱动程序的初始化函数并获取其函数。其中一些必须被过滤掉,一些必须被我的替换,一些则按原样接受。
在某个时候,我决定在我的过滤器函数中使用 SEH(结构化异常处理);这意味着——每个这样的顶层函数都必须用 SEH 处理程序包装。因为我已经在使用宏来处理我的函数,所以只需一分钟时间,我就为我所有的函数添加了 SEH 处理程序!
顺便说一句,这次我使用了几个宏来列出特定类别的函数:那些必须始终被挂钩的,那些必须被替换的,等等,而不是像上一例中的 COMM_MSGS_All
那样用一个宏列出所有函数。
我不会在这里列出代码,它太复杂了,需要很多解释。但是,您可以相信我,这是一种享受。我没有陷入数万行重复代码的麻烦,而只需要编写一个优雅的宏来完成所有事情。
注意,在这种情况下,也没有 C++ 多态性(运行时参数列表等)或模板函数的替代方案。
回调编组
编组 - 我的意思是,我有在某个线程中调用的回调函数。我希望它们将消息发布到另一个线程并立即返回。然后,在另一个线程中,我希望调用相应回调函数并传入所有必需的参数。
在不涉及过多细节的情况下,我就是这样实现的
#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)
// ...
注意:与前面的例子不同,每个参数的参数列表现在有两种类型:第一种是我们回调函数根据约定的原型应接收的类型。第二种是可以用来编组此参数的变量的类型。对于序数类型(ULONG
、UCHAR
等),它是相同的。但对于 PCTSTR
之类的内容,则不同:您不能只获取指针,还需要创建字符串的副本。
让我们声明我们想要调用的函数
#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
另一个区别:每个参数后面都有sep(也通过宏参数传递)。由于这个原因,我们可以用逗号分隔上面函数声明中的参数。
现在,每当调用我们的回调函数时,我们就分配一个带有必需参数的任务结构并将其发布到我们的专用线程。让我们写下来
#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)); \
}
等等。
其他一些例子
我见过大量代码这样编写
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 ...
使用宏,我们可以将其转换为
#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)
// ...
当您编写 MFC 基于对话框的应用程序时,您可以通过所谓的 DDX 机制获取/设置控件的值。对于每个此类控件,您可以通过向导声明一个变量,向导会自动将其添加到 DoDataExchange
函数并在构造函数中零初始化它。
但很多时候,我也需要向导没有自动实现的东西:将这些参数存储在注册表中并读回。宏再次帮助了我。
我曾经需要构建/解析 XML(我讨厌 XML),它需要“特殊”字符编码。我声明了那些“特殊”字符的编码规则
#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")
它们需要被分成几类,因为对于属性和常规 XML 节点,编码字符的规则略有不同。
现在,每当我需要编码/解码 XML 字符串时,我只需声明 XML_SPECIAL_CHAR
来执行相应操作,并列出所有特殊字符,这些字符与当前情况相关。
我通过宏实现的另一个很棒的功能:我们有一个 Direct3D 应用程序,它执行大量绘图。有一次我需要记录所有绘图,同时尽量少影响性能。通过使用宏,我用代码包装了所有 Direct3D 函数,该代码除了实际绘图外,还记录(顺便说一句,类似于消息示例)所有函数调用及其所有相关参数。然后,通过再次使用宏,可以解析此流并“重放”所有函数。
结论
有时,可以使用模板函数/类。如果可以,当然,它们是首选方法。但通常,情况更糟。
通过这样的宏使用,我的代码几乎没有重复。在我看来,重复是编程中最糟糕的事情。我意识到使用宏的所有威胁,并且我仍然更喜欢宏而不是无限的代码重写。是的,宏就像拼图,有时不易理解。但它们使程序非常紧凑。
我也倾向于使用宏来以一种超级多态的方式重新组织代码,以尽量减少重写,但这只是我的观点,我不坚持。这一切最终都是一个意见问题。
虽然在我读过的所有书籍和文章中,宏的使用都受到批评,但我指出了它们的一些确定优势。是否值得使用它们——决定权在你。
一如既往,欢迎评论。无论积极还是消极。