为什么预处理器宏是邪恶的,有什么替代方案?

use*_*534 85 c++ c-preprocessor c++11

我一直都这么问,但我从来没有得到过一个非常好的答案; 我认为,在写第一个"Hello World"之前,几乎所有程序员都遇到过"宏不应该使用宏","宏是邪恶的"这样的短语等等,我的问题是:为什么?有了新的C++ 11,这么多年后还有一个真正的选择吗?

简单的部分是关于宏#pragma,特定于平台和特定于编译器,并且大多数时候它们具有严重的缺陷,例如#pragma once在至少两种重要情况下容易出错:不同路径中的相同名称以及一些网络设置和文件系统.

但总的来说,宏的用法和替代品呢?

Mat*_*son 148

宏就像任何其他工具一样 - 谋杀中使用的锤子不是邪恶的,因为它是锤子.以这种方式使用它的方式是邪恶的.如果你想锤击钉子,锤子是一个完美的工具.

宏的一些方面使它们"变坏"(我将在后面进行扩展,并建议替代方案):

  1. 您无法调试宏.
  2. 宏观扩张会导致奇怪的副作用.
  3. 宏没有"命名空间",所以如果你有一个与其他地方使用的名字冲突的宏,你会得到你不想要它的宏替换,这通常会导致奇怪的错误消息.
  4. 宏可能会影响您没有意识到的事情.

那么让我们在这里扩展一下:

1)无法调试宏. 如果您有一个转换为数字或字符串的宏,源代码将具有宏名称和许多调试器,您无法"看到"宏转换为什么.所以你实际上并不知道发生了什么.

更换:使用enumconst T

对于"类似函数"的宏,因为调试器在"你所在的每个源代码行"上运行,所以无论是一个语句还是一百个语句,你的宏都会像单个语句一样工作.很难弄清楚发生了什么.

替换:使用函数 - 内联如果它需要"快速"(但要注意太多的内联不是一件好事)

2)宏扩展可能会产生奇怪的副作用.

着名的是#define SQUARE(x) ((x) * (x))和使用x2 = SQUARE(x++).这导致x2 = (x++) * (x++);,即使它是有效的代码[1],几乎肯定不会是程序员想要的.如果它是一个函数,那么执行x ++会很好,而x只会递增一次.

另一个例子是宏中的"if else",比方说我们有:

#define safe_divide(res, x, y)   if (y != 0) res = x/y;
Run Code Online (Sandbox Code Playgroud)

然后

if (something) safe_divide(b, a, x);
else printf("Something is not set...");
Run Code Online (Sandbox Code Playgroud)

它实际上完全是错误的....

替换:真正的功能.

3)宏没有命名空间

如果我们有一个宏:

#define begin() x = 0
Run Code Online (Sandbox Code Playgroud)

我们在C++中有一些使用begin的代码:

std::vector<int> v;

... stuff is loaded into v ... 

for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
   std::cout << ' ' << *it;
Run Code Online (Sandbox Code Playgroud)

现在,你认为你得到了什么错误信息,你在哪里寻找错误[假设你已经完全忘记 - 或者甚至不知道 - 生活在其他人写的某个头文件中的开始宏?[如果你在包含之前加入那个宏,那就更有趣了 - 当你看到代码本身时,你会被淹没在奇怪的错误中,这完全没有意义.

替换:那么替换作为"规则"并不多 - 只使用宏的大写名称,并且永远不会将所有大写名称用于其他内容.

4)宏有你没有意识到的效果

采取这个功能:

#define begin() x = 0
#define end() x = 17
... a few thousand lines of stuff here ... 
void dostuff()
{
    int x = 7;

    begin();

    ... more code using x ... 

    printf("x=%d\n", x);

    end();

}
Run Code Online (Sandbox Code Playgroud)

现在,在不查看宏的情况下,您会认为begin是一个函数,它不应该影响x.

这种事情,我已经看到了更复杂的例子,真的可以搞乱你的一天!

替换:要么不使用宏来设置x,要么将x作为参数传递.

有时使用宏肯定是有益的.一个例子是用宏包装一个函数来传递文件/行信息:

#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x)  my_debug_free(x, __FILE__, __LINE__)
Run Code Online (Sandbox Code Playgroud)

现在我们可以my_debug_malloc在代码中使用常规malloc,但是它有额外的参数,所以当它结束并且我们扫描"哪些内存元素没有被释放"时,我们可以打印分配的位置,这样程序员可以追踪泄漏.

[1]在"序列点"中多次更新一个变量是未定义的行为.序列点与语句不完全相同,但对于大多数意图和目的,我们应该将其视为.这样做x++ * x++会更新x两次,这是未定义的,可能会导致不同系统上的不同值,以及不同的结果值x.

  • @AaronMcDaid:是的,有一些解决方法可以解决这些宏中暴露的一些问题.我的帖子的重点不是要展示如何做好宏,而是"让宏出错是多么容易",哪里有一个很好的选择.也就是说,有些东西很容易解决宏,有时宏也是正确的做法. (9认同)
  • 宏很难翻译成其他语言. (5认同)
  • 可以通过将宏体包装在`do {...} while(0)`中来解决`if else`问题.这表现为人们对`if`和`for`以及其他潜在风险控制流问题的预期.但是,是的,真正的功能通常是更好的解决方案.`#define macro(arg1)do {int x = func(arg1); FUNC2(X0); } while(0)` (4认同)
  • @AShelly:你将这些用途称为什么?我在我“处理过”的代码中几乎见过这些东西(不一定是我认识的人写的)。当然,他们不应该这样做,但事实上,这些完全可能是我所说的宏的邪恶之处。请随时就如何改进这个答案提出建议。 (2认同)

utn*_*tim 21

俗称"宏是邪恶的"通常是指使用#define,而不是#pragma.

具体来说,表达式指的是这两种情况:

  • 将幻数定义为宏

  • 使用宏来替换表达式

使用新的C++ 11后,这么多年后还有一个真正的替代方案吗?

是的,对于上面列表中的项目(幻数应该用const/constexpr定义,表达式应该用[normal/inline/template/inline template]函数定义.

以下是将幻数定义为宏并使用宏替换表达式(而不是定义用于评估这些表达式的函数)引入的一些问题:

  • 在为幻数定义宏时,编译器不会保留定义值的类型信息.这可能会导致编译警告(和错误)并使调试代码的人感到困惑.

  • 在定义宏而不是函数时,使用该代码的程序员希望它们像函数一样工作,而不是.

考虑以下代码:

#define max(a, b) ( ((a) > (b)) ? (a) : (b) )

int a = 5;
int b = 4;

int c = max(++a, b);
Run Code Online (Sandbox Code Playgroud)

你会期望a和c在赋值给c之后为6(就像使用std :: max而不是宏一样).相反,代码执行:

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7
Run Code Online (Sandbox Code Playgroud)

除此之外,宏不支持名称空间,这意味着在代码中定义宏将限制客户端代码以他们可以使用的名称.

这意味着如果您定义上面的宏(最大值),您将无法再使用#include <algorithm>下面的任何代码,除非您明确写入:

#ifdef max
#undef max
#endif
#include <algorithm>
Run Code Online (Sandbox Code Playgroud)

使用宏而不是变量/函数也意味着您无法获取其地址:

  • 如果宏作为常量计算为幻数,则不能通过地址传递它

  • 对于宏作为函数,您不能将其用作谓词或使用函数的地址或将其视为函子.

编辑:作为一个例子,正确的替代#define max上述:

template<typename T>
inline T max(const T& a, const T& b)
{
    return a > b ? a : b;
}
Run Code Online (Sandbox Code Playgroud)

这可以完成宏所做的一切,但有一个限制:如果参数的类型不同,模板版本会强制您显式化(这实际上会导致更安全,更明确的代码):

int a = 0;
double b = 1.;
max(a, b);
Run Code Online (Sandbox Code Playgroud)

如果将此max定义为宏,则代码将编译(带有警告).

如果将此max定义为模板函数,编译器将指出歧义,您必须说max<int>(a, b)或者max<double>(a, b)(并因此明确说明您的意图).


pha*_*zon 12

一个常见的麻烦是:

#define DIV(a,b) a / b

printf("25 / (3+2) = %d", DIV(25,3+2));
Run Code Online (Sandbox Code Playgroud)

它将打印10而不是5,因为预处理器将以这种方式扩展它:

printf("25 / (3+2) = %d", 25 / 3 + 2);
Run Code Online (Sandbox Code Playgroud)

这个版本更安全:

#define DIV(a,b) (a) / (b)
Run Code Online (Sandbox Code Playgroud)

  • `#define DIV(a,b)(a)/(b)`不够好; 作为一般惯例,总是添加最外面的括号,如下所示:`#define DIV(a,b)((a)/(b))` (5认同)
  • 有趣的例子,基本上它们只是没有语义的标记 (2认同)
  • 您的意思是`#define DIV(a,b)`,而不是`#define DIV(a,b)`,这是非常不同的。 (2认同)