浅谈C语言中的X宏函数

开课吧小一2021-04-02 15:18

介绍

在这篇文章中,我想解释什么是X宏,以及它们是如何工作的。

我知道这不是一个新的想法,但我在网上没有找到很多文档。你可以在下面找到一个谈论或使用X宏的文章列表。

- http://www.codeproject.com/Articles/25541/C-C-macros-programming

- http://stackoverflow.com/questions/147267/easy-way-to-use-variables-of-enum-types-as-string-in-c

- http://www.drdobbs.com/the-new-c-x-macros/184401387

这里的想法是尽可能简单地描述这种技术。我不会告诉你如何使用它们,而是简单地用简单的例子来描述它们,我相信你会发现你的项目的实用性。

几年前我发现了X宏,我发现它们非常强大和有用。

说明

背后的基础知识

X 宏背后的概念是基于将宏作为参数传递给另一个宏的可能性。

让我们看看下面的宏声明。

C++
Copy Code
#define APPLY_A_MACRO_ON_SECOND_PARAMETER(PARAM_MACRO, parameter) PARAM_MACRO(parameter)

由于它的声明,宏APPLY_A_MACRO_ON_SECOND_PARAMETER将展开为。

C++
Copy Code
PARAM_MACRO(parameter)

这里没什么复杂的。但现在,你可以问什么是PARAM_MACRO?

第一个例子

这个时候,PARAM_MACRO什么都不是,因为它还没有被声明。而这正是这个技术的强大之处。

现在,我们可以声明不同类型的宏,只适用于一个参数,并将它们传递给APPLY_A_MACRO_ON_SECOND_PARAMETER。

C++
Copy Code
// Let's declare two different macros

// first one will declare one integer with parameterized name 
#define PARAMETER_TO_INT_DECL(parameter) int parameter;

// second one will create a getter function that will return the variable
#define PARAMETER_TO_GETTER_FUNC(parameter) int get_##parameter() { return parameter; }

现在,我们可以用这两个宏调用APPLY_A_MACRO_ON_SECOND_PARAMETER。

从PARAMETER_TO_INT_DECL开始。

C++
Copy Code
APPLY_A_MACRO_ON_SECOND_PARAMETER(PARAMETER_TO_INT_DECL, width)

1.APPLY_A_MACRO_ON_SECOND_PARAMETER这样展开。

C++
Copy Code
PARAM_MACRO(parameter)

2.现在让我们用参数值代替。

C++
Copy Code
PARAMETER_TO_INT_DECL(width)

3.PARAMETER_TO_INT_DECL这样展开。

C++
Copy Code
int parameter;

4.现在让我们用最后一次扩展的参数值来代替。

C++
Copy Code
int width;

我们现在来看一下用PARAMETER_TO_GETTER_FUNC的扩展。

C++
Copy Code
APPLY_A_MACRO_ON_SECOND_PARAMETER(PARAMETER_TO_GETTER_FUNC, width)

1.APPLY_A_MACRO_ON_SECOND_PARAMETER这样展开。

C++
Copy Code
PARAM_MACRO(parameter)

2.现在让我们用参数值代替。

C++
Copy Code
PARAMETER_TO_GETTER_FUNC(width)

3.PARAMETER_TO_GETTER_FUNC这样展开。

C++
Copy Code
int get_##parameter() { return parameter; }

4.现在让我们用最后一次扩展的参数值来代替。

C++
Copy Code
int get_width() { return width; }

综上所述:

C++
Copy Code
APPLY_A_MACRO_ON_SECOND_PARAMETER(PARAMETER_TO_INT_DECL, width)
APPLY_A_MACRO_ON_SECOND_PARAMETER(PARAMETER_TO_GETTER_FUNC, width)

// will turn into
int width;
int get_width() { return width;

这个例子可能不会给你留下什么印象。我留了基本的,但我们可以说:我们已经创造了一种自动标准化getters函数的方法。

它的使用仍然有点复杂,所以我们可以声明一个新的宏,比如这个。

C++
Copy Code
#define INT_VAR_PLUS_GETTER(variable_name)\
    APPLY_A_MACRO_ON_SECOND_PARAMETER(PARAMETER_TO_INT_DECL, variable_name)\
    APPLY_A_MACRO_ON_SECOND_PARAMETER(PARAMETER_TO_GETTER_FUNC, variable_name)

现在,让我们调用INT_VAR_PLUS_GETTER。

C++
Copy Code
INT_VAR_PLUS_GETTER(width)

// will turn into
int width;
int get_width() { return width; }

现在,我们有了一个宏库,允许在一次调用中同时声明变量和getter。一切都标准化了。

在这一点上,我相信你们中的大多数人仍然没有被打动。

对于这样一个简单的(也许是没用的)结果,这么复杂的东西有什么意义呢?

让我们转到一个更复杂和有用的例子。

有用的例子:枚举转字符串

动机

正如我在介绍中所说,我在几年前发现了X宏技术。当时,我正在维护一个大型C项目的代码。

在这个项目中,使用了大量的枚举,我想添加日志,以便在一些错误发生时追踪枚举的值。

最开始,我是这样记录的。

C++
Copy Code
printf("enum value : %d\n", enum_value);

但是这样做的问题是当enum_value的enum类型有一百多个条目时。

假设你有这样的日志要解释:"enum value : 57"。

然后你必须找到enum声明,并计算条目,直到你到达第57个条目。这是很痛苦的。此外,57并不是一个足够的关于枚举类型的信息数据。我们只是希望有一个枚举,允许将57转化为一个可读的字符串。

让我们不用宏来做

首先,我们将在没有宏帮助的情况下实现这样一个函数,以便了解X宏如何帮助我们实现工作自动化和避免错误。

让我们从一个简单的枚举声明开始。

C++
Copy Code
typedef enum IceCreamFlavors
{
    CHOCOLATE = 56,
    VANILLA = 27,
    PISTACHIO = 72,
} 
IceCreamFlavors;

现在,让我们定义将枚举值转化为字符串的函数。

C++
Copy Code
const char* IceCreamFlavors_toString(IceCreamFlavors flavor)
{
    switch(flavor)
    {
    case CHOCOLATE:
        return "CHOCOLATE";
    case VANILLA:
        return "VANILLA";
    case PISTACHIO:
        return "PISTACHIO";
    default:
        // the error handling might seem a bit too strict !
        return 0;
        // you can also return something like:
        return "## unknown IceCreamFlavors value ##";
    }
}

创建和维护这个_toString函数是有点重复的。你很容易犯复制/粘贴错误或者忘记一个条目。此外,当你更新枚举时,你必须确保_toString函数被正确更新。这是个潜在的错误来源。

现在,让我们看看X宏如何帮助我们。

我们需要的是在代码中的一个位置来存储枚举值,我们希望_toString函数存在,并根据枚举值自动更新。而且我们希望_toString函数存在,并根据枚举值自动更新。

存储Enum值

首先,我们创建一个宏来存储Enum值。

C++
Copy Code
#define SMART_ENUM_IceCreamFlavors(_)\
    _(CHOCOLATE, 56)\
    _(VANILLA, 27)\
    _(PISTACHIO, 72)

记住描述章节。我们在这里所做的是声明一个宏SMART_ENUM_IceCreamFlavors,它以另一个宏(_)作为参数。这将允许我们用给_的参数做我们想做的事情。

Enum Declaration

首先,我们要用C语言的方式来声明枚举。

我们首先需要一个宏,将SMART_ENUM_IceCreamFlavors项(行下)变成C枚举项。

C++
Copy Code
#define SMART_ENUM_ENTRY(entry_name, entry_value) entry_name = entry_value,

然后,我们需要一个宏来构建整个C枚举。

C++
Copy Code
// the macro takes the enum macro definition as parameter 
// (in our case we will pass SMART_ENUM_IceCreamFlavors)
#define SMART_ENUM_DECLARE_ENUM(MACRO_DEFINITION, enum_name)\
typedef enum enum_name\
{\
    MACRO_DEFINITION(SMART_ENUM_ENTRY)\
}\
enum_name;

现在,让我们调用这个宏,看看扩展。

C++
Copy Code
// the first parameter is the macro definition of the enum, 
// the second one is the name we want to give to the enum
SMART_ENUM_DECLARE_ENUM(SMART_ENUM_IceCreamFlavors, IceCreamFlavors)

1.SMART_ENUM_DECLARE_ENUM这样展开。

C++
Copy Code
typedef enum enum_name\
{\
   MACRO_DEFINITION(SMART_ENUM_ENTRY)\
}\
enum_name;

2.用宏参数代替:

C++
Copy Code
typedef enum IceCreamFlavors
{
    SMART_ENUM_IceCreamFlavors(SMART_ENUM_ENTRY)
}
IceCreamFlavors;

3.扩大 SMART_ENUM_IceCreamFlavors:

C++
Copy Code
typedef enum IceCreamFlavors 
{
    _(CHOCOLATE, 56)\
    _(VANILLA, 27)\
    _(PISTACHIO, 72)
} 
IceCreamFlavors;

4.并在参数中替换为宏。

C++
Copy Code
typedef enum IceCreamFlavors
{
    SMART_ENUM_ENTRY(CHOCOLATE, 56)
    SMART_ENUM_ENTRY(VANILLA, 27)
    SMART_ENUM_ENTRY(PISTACHIO, 72) 
}
IceCreamFlavors;

5.SMART_ENUM_ENTRY 拓展

C++
Copy Code
entry_name = entry_value,

6.最终结果:

C++
Copy Code
typedef enum IceCreamFlavors 
{
    CHOCOLATE = 56,
    VANILLA = 27,
    PISTACHIO = 72,
} 
IceCreamFlavors;

_创建toString函数

现在,我们要创建这样一个类似的宏,以便用同样的宏定义enum来创建_toString函数。这样,我们将只有一个存储枚举值的位置 !

让我们从这个宏开始,它将把宏定义的每个条目变成函数的case语句。

C++
Copy Code
// the macro takes two parameters as the macro definition use macro that takes two
// so entry value is not use, but it is not a big deal
#define SMART_ENUM_TOSTRING_CASE(entry_name, entry_value) case entry_name: return #entry_name;

现在,让我们写一个宏,来构建整个_toString函数。:

C++
Copy Code
// the macro takes the enum macro definition as parameter 
// (in our case we will pass SMART_ENUM_IceCreamFlavors) 
#define SMART_ENUM_DEFINE_TOSTRING_FUNCTION(MACRO_DEFINITION, enum_name)\
const char* enum_name##_toString(enum_name enum_value)\
{\
    switch(enum_value)\
    {\
    MACRO_DEFINITION(SMART_ENUM_TOSTRING_CASE)\
    default:\
        // the error handling might seem a bit too strict !\
        return 0;\
        // you can also return something like:\
        return "## unknown enum_name value ##";\
    }\
}

现在,让我们调用这个宏,看看扩展。

C++
Copy Code
// the first parameter is the macro definition of the enum,
// the second one is the name we want to give to the enum
SMART_ENUM_DEFINE_TOSTRING_FUNCTION(SMART_ENUM_IceCreamFlavors, IceCreamFlavors)

1.SMART_ENUM_DEFINE_TOSTRING_FUNCTION 扩展:

C++
Copy Code
const char* enum_name##_toString(enum_name enum_value)\
{\
    switch(enum_value)\
    {\
    MACRO_DEFINITION(SMART_ENUM_TOSTRING_CASE)\
    default:\
        // the error handling might seem a bit too strict !\
        return 0;\
        // you can also return something like:\
        return "## unknown enum_name value ##";\
    }\
}

2.用宏替代参数:

C++
Copy Code
const char* IceCreamFlavors_toString(IceCreamFlavors enum_value)
{
    switch(enum_value)
    {
    SMART_ENUM_IceCreamFlavors(SMART_ENUM_TOSTRING_CASE)
    default:
        // the error handling might seem a bit too strict !
        return 0;
        // you can also return something like:
        return "## unknown IceCreamFlavors value ##";
    }
}

3.扩展 SMART_ENUM_IceCreamFlavors:

C++
Copy Code
const char* IceCreamFlavors_toString(IceCreamFlavors enum_value)
{
    switch(enum_value)
    {
    _(CHOCOLATE, 56)\
    _(VANILLA, 27)\ 
    _(PISTACHIO, 72)
    default:
        // the error handling might seem a bit too strict !
        return 0;
        // you can also return something like:
        return "## unknown IceCreamFlavors value ##";
    }
}

4.用宏替代参数:

C++
Copy Code
const char* IceCreamFlavors_toString(IceCreamFlavors enum_value)
{
    switch(enum_value)
    {
    SMART_ENUM_TOSTRING_CASE(CHOCOLATE, 56)
    SMART_ENUM_TOSTRING_CASE(VANILLA, 27)
    SMART_ENUM_TOSTRING_CASE(PISTACHIO, 72)
    default:
        // the error handling might seem a bit too strict !
        return 0;
        // you can also return something like:
        return "## unknown IceCreamFlavors value ##";
    }
}

5.SMART_ENUM_TOSTRING_CASE扩展:

C++
Copy Code
case entry_name: return #entry_name;

 

6.最后,扩展,呈现结果

C++
Copy Code
const char* IceCreamFlavors_toString(IceCreamFlavors enum_value)
{
    switch(enum_value)
    {
    case CHOCOLATE: return "CHOCOLATE";
    case VANILLA: return "VANILLA";
    case PISTACHIO: return "PISTACHIO";
    default:
        // the error handling might seem a bit too strict !
        return 0;
        // you can also return something like:
        return "## unknown IceCreamFlavors value ##";
   }
}

我们用创建枚举声明的同一个宏定义来构建_toString函数。

最后的宏

现在我们有了所有的工具,让我们创建最后一个宏。

C++
Copy Code
#define DEFINE_SMART_ENUM(MACRO_DECLARATION, enum_name)\
    SMART_ENUM_DECLARE_ENUM(MACRO_DECLARATION, enum_name)\
    SMART_ENUM_DEFINE_TOSTRING_FUNCTION(MACRO_DECLARATION, enum_name)
C++
Shrink ▲   Copy Code
// I rewrite the macro declaration here to remember
#define SMART_ENUM_IceCreamFlavors(_)\
    _(CHOCOLATE, 56)\
    _(VANILLA, 27)\
    _(PISTACHIO, 72)

// we call the builder
DEFINE_SMART_ENUM(SMART_ENUM_IceCreamFlavors, IceCreamFlavors)

// the result will be...

typedef enum IceCreamFlavors 
{
    CHOCOLATE = 56,
    VANILLA = 27,
    PISTACHIO = 72,
}
IceCreamFlavors;

const char* IceCreamFlavors_toString(IceCreamFlavors enum_value)
{
    switch(enum_value)
    {
    case CHOCOLATE: return "CHOCOLATE";
    case VANILLA: return "VANILLA";
    case PISTACHIO: return "PISTACHIO";
    default:
        // the error handling might seem a bit too strict !
        return 0;
        // you can also return something like:
        return "## unknown IceCreamFlavors value ##";
    }
}

我们现在有了一个漂亮而强大的单行构建器来。

1.确保枚举声明和_toString函数之间的深度联系。

2.减少编码时间

3.避免愚蠢的错误

当然,每次对宏定义的更新都会对枚举声明和_toString函数产生影响。

结论

我希望这篇文章足够清晰,不要太无聊。我试图让它尽可能清晰,所以这解释了它的大小。宏扩展可能很难理解。经常发生的情况是,我必须在心里重新展开宏,才能理解它们的作用。

说到这里,我真的认为X宏是一个很棒的工具。

我读到过很多针对该技术的批评,因为它使用的宏被认为是不安全的。

我完全同意,使用宏是很敏感的,我不建议在所有事情上都使用它们。

但我认为那是一种语言扩展,不能用其他方式实现。

如果你看看 "Enum转字符串 "的例子,构建的代码总是很简单。宏并不是隐藏了一些我们需要调试的关键代码。它应该被用来自动完成一些众所周知的简单机制,这些机制在宏化之前已经被测试过。

更多C/C++教程尽在开课吧C/C++教程频道

有用
分享
全部评论快来秀出你的观点
登录 后可发表观点…
发表
暂无评论,快来抢沙发!
算法刷题核心能力提升营