MACRO max & MACRO min
早闻宏max和宏min的副作用非常严重。《Effective C++》简体中文第三版“条款03:尽可能使用const”Page16页有详细的例证:
//以a和b的较大值调用f
#define CALL_WITH_MAX(a, b) f((a) > (b)) ? (a) : b())
这般长相的宏有着太多的缺点,光是想到它们就让人痛苦不堪。
无论何时当你写出这种宏,你必须记住为宏中的所有实参加上小括号,否则某些人在表达式中调用这个宏时可能会遭遇麻烦。但纵使你为所有的实参加上小括号,看看下面不可思议的事情:
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); //a被累加二次
CALL_WITH_MAX(++a, b+10); //a被累加一次
在这里,调用f之前,a的递增次数竟然取决于“它被拿来和谁比较”!
好了,现在看看VS2008自带的头文件中关于max和min宏定义:
Windef.h line 187
#ifndef NOMINMAX
#ifndef max
#define max(a,b) (((a) > (b)) ? (a) : (b))
#endif
#ifndef min
#define min(a,b) (((a) < (b)) ? (a) : (b))
#endif
#endif /* NOMINMAX */
这些宏简直要和《Effective C++》所鄙视的做法一模一样了!如果用到了max和min宏,切记切记不要在传参的时候做任何可能带来副作用事情。我的建议是,如果用到max和min宏,仅仅传给他们简单的数字,包括+1这样的运算都最好在传入之前计算完毕。
下面再看看max和min宏对性能产生的不利影响。不利影响?宏不是因为避免了函数调用而提高了性能吗?答案是否。宏就是宏,简单的文字游戏,性能是需要程序员去控制的,而不是宏。测试代码如下(为了避免max、min宏和std::max、std::min名字冲突,我把它们添加了前缀“macro_”):
#ifndef macro_max
#define macro_max(a,b) (((a) > (b)) ? (a) : (b))
#endif
#ifndef macro_min
#define macro_min(a,b) (((a) < (b)) ? (a) : (b))
#endif
#include <iostream>
void main()
{
const char* str = "012345";
int n = 0;
n = macro_min(strlen(str), 100);
std::cout << n << std::endl;
n = std::min(strlen(str), 100U);
std::cout << n << std::endl;
}
下面是VS2008 RELEASE下的汇编代码(下面代码的批注我以“@”开头了):
#ifndef macro_max
#define macro_max(a,b) (((a) > (b)) ? (a) : (b))
#endif
#ifndef macro_min
#define macro_min(a,b) (((a) < (b)) ? (a) : (b))
#endif
#include <iostream>
void main()
{
const char* str = "012345";
int n = 0;
n = macro_min(strlen(str), 100); @先看看宏的表现如何
00401000 mov eax,offset string "012345" (402114h) @给strlen压入参数,开始strlen的调用。经过VS强大的优化之后,strlen被完全内联了,所以你看不到call了
00401005 sub esp,8
00401008 lea edx,[eax+1]
0040100B jmp main+10h (401010h)
0040100D lea ecx,[ecx]
00401010 mov cl,byte ptr [eax]
00401012 inc eax
00401013 test cl,cl
00401015 jne main+10h (401010h)
00401017 sub eax,edx @第一次的strlen调用到这里结束
00401019 cmp eax,64h @这里是宏中那个三目运算符“?”的执行,64h就是源码中的100
0040101C jae main+31h (401031h)
0040101E mov eax,offset string "012345" (402114h) @重新给strlen压入参数,并开始strlen的调用
00401023 lea edx,[eax+1]
00401026 mov cl,byte ptr [eax]
00401028 inc eax
00401029 test cl,cl
0040102B jne main+26h (401026h)
0040102D sub eax,edx @到这里第二次的strlen调用结束
0040102F jmp main+36h (401036h)
00401031 mov eax,64h
std::cout << n << std::endl; @这句必须有,否则前面后面的代码都会被编译器优化给裁掉的,这句cout告诉编译器“刀下留人”。不信的话你可以自己试一下。
00401036 mov ecx,dword ptr [__imp_std::endl (402040h)]
0040103C push ecx
0040103D mov ecx,dword ptr [__imp_std::cout (402044h)]
00401043 push eax
00401044 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (40203Ch)]
0040104A mov ecx,eax
0040104C call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)]
n = std::min(strlen(str), 100U); @下面该STL std::min模板算法表演了
00401052 mov eax,offset string "012345" (402114h) @同上,给strlen压入参数
00401057 mov dword ptr [esp],64h
0040105E lea edx,[eax+1]
00401061 mov cl,byte ptr [eax]
00401063 inc eax
00401064 test cl,cl
00401066 jne main+61h (401061h)
00401068 sub eax,edx @strlen到此执行完毕
0040106A mov dword ptr [esp+4],eax @随后比较比较结果就出来了
0040106E cmp eax,64h
00401071 lea eax,[esp]
00401074 ja main+7Ah (40107Ah)
00401076 lea eax,[esp+4]
std::cout << n << std::endl; @同上,此处的cout不可省略啊
0040107A mov edx,dword ptr [__imp_std::endl (402040h)]
00401080 mov eax,dword ptr [eax]
00401082 mov ecx,dword ptr [__imp_std::cout (402044h)]
00401088 push edx
00401089 push eax
0040108A call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (40203Ch)]
00401090 mov ecx,eax
00401092 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)]
}
00401098 xor eax,eax @这显然时main的返回代码了
0040109A add esp,8
0040109D ret
仅以最直观的方式就可以发现,即使编译器进行了大量的优化,macro_min还是对strlen(str)这个函数进行了两次调用,而这是和程序员本应想表达的意思相违背的。既然已经违背了程序员的本意,那啥事还不能发生呢?如果是strlen还好说,至多浪费一次时间复杂度为O(n)的函数调用,如果macro_min(f1, f2)中的f1、f2可以修改程序的状态,那多执行一次会带来什么后果恐怕只有崩溃后去猜了。
鉴于以上血淋淋的事实。Windows下编程,请不要再使用max和min宏了。在你的预编译中加入NOMINMAX定义、或者将NOMINMAX加入到预编译头文件、或者显式的采用#undef max #undef min直接将二者干掉。转而#include<algorithm>,但别using namespace std;很嚣张的写上std::max or std::min,明确的告诉编译器和你自己,你调用的是STL的算法。
评论