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

时间:2012-12-26 13:44:52

标签: c++ c++11 c-preprocessor

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

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

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

8 个答案:

答案 0 :(得分:134)

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

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

  1. 您无法调试宏。
  2. 宏扩展会导致奇怪的副作用。
  3. 宏没有“命名空间”,所以如果你有一个与其他地方使用的名字冲突的宏,你会得到你不想要的宏替换,这通常会导致奇怪的错误信息。
  4. 宏可能会影响您没有意识到的事情。
  5. 所以让我们在这里扩展一下:

    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;
    

    然后

    if (something) safe_divide(b, a, x);
    else printf("Something is not set...");
    

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

    替换:真正的功能。

    3)宏没有名称空间

    如果我们有一个宏:

    #define begin() x = 0
    

    我们在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;
    

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

    替换:除了替换“规则”之外没有那么多 - 只使用宏的大写名称,并且永远不要将所有大写名称用于其他内容。

    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();
    
    }
    

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

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

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

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

    #define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
    #define free(x)  my_debug_free(x, __FILE__, __LINE__)
    

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

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

答案 1 :(得分: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);

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

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7

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

这意味着如果你定义上面的宏(最大值),你将不再能够在下面的任何代码中#include <algorithm>,除非你明确写道:

#ifdef max
#undef max
#endif
#include <algorithm>

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

  • 如果宏观常量计算为幻数,则无法通过地址传递

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

修改:例如,上面#define max的正确替代方法:

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

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

int a = 0;
double b = 1.;
max(a, b);

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

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

答案 2 :(得分:11)

常见的麻烦是:

#define DIV(a,b) a / b

printf("25 / (3+2) = %d", DIV(25,3+2));

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

printf("25 / (3+2) = %d", 25 / 3 + 2);

此版本更安全:

#define DIV(a,b) (a) / (b)

答案 3 :(得分:3)

宏特别适用于创建通用代码(宏的参数可以是任何东西),有时候还带有参数。

此外,此代码在使用宏的位置放置(即插入)。

OTOH,可能会得到类似的结果:

  • 重载函数(不同参数类型)

  • 模板,C ++(通用参数类型和值)

  • 内联函数(将代码放在它们被调用的地方,而不是跳转到单点定义 - 但是,这是编译器的推荐)。

编辑:关于为什么宏不好:

1)没有对参数进行类型检查(它们没有类型),因此很容易被误用 2)有时会扩展为非常复杂的代码,在预处理文件中很难识别和理解 3)很容易在宏中创建容易出错的代码,例如:

#define MULTIPLY(a,b) a*b

然后致电

MULTIPLY(2+3,4+5)

扩展

2 + 3 * 4 + 5(而不是:(2 + 3)*(4 + 5))。

要拥有后者,您应该定义:

#define MULTIPLY(a,b) ((a)*(b))

答案 4 :(得分:2)

我不认为在调用它们时使用预处理器定义或宏有任何问题。

它们是c / c ++中的(元)语言概念,与任何其他工具一样,如果您知道自己在做什么,它们可以让您的生活更轻松。宏的问题在于它们在您的c / c ++代码之前被处理并生成可能有问题的新代码并导致编译器错误,这些错误很明显。从好的方面来看,它们可以帮助您保持代码清洁,如果使用得当,可以为您节省大量的打字,因此可归结为个人喜好。

答案 5 :(得分:1)

我认为问题在于编译器没有很好地优化宏,并且读取和调试“丑陋”。

通常,一个好的选择是泛型函数和/或内联函数。

答案 6 :(得分:1)

C / C ++中的宏可以作为版本控制的重要工具。可以使用较小的宏配置将相同的代码传递给两个客户端。我使用像

这样的东西
#define IBM_AS_CLIENT
#ifdef IBM_AS_CLIENT 
  #define SOME_VALUE1 X
  #define SOME_VALUE2 Y
#else
  #define SOME_VALUE1 P
  #define SOME_VALUE2 Q
#endif

如果没有宏,这种功能就不那么容易了。宏实际上是一个很棒的软件配置管理工具,而不仅仅是一种方法 创建重用代码的快捷方式。定义功能的目的 宏中的可重用性肯定会产生问题。

答案 7 :(得分:0)

预处理器宏用于以下目的时并不邪恶:

  • 使用#ifdef类型的结构创建同一软件的不同发行版,例如针对不同区域的Windows发行版。
  • 用于定义与代码测试相关的值。

替代方案 出于类似的目的,可以使用ini,xml,json格式的某种配置文件。但是使用它们会对代码产生运行时影响,预处理器宏可以避免。