在C中用宏创建假函数可以多少钱?

pab*_*977 9 c macros c99

人们总是说宏是不安全的,而且它们不是(直接)对其参数进行类型检查,等等.更糟糕的是:当发生错误时,编译器会给出内在和难以理解的诊断,因为宏只是一团糟.

是否有可能以与函数几乎相同的方式使用宏,通过安全的类型检查,避免典型的陷阱以及编译器提供正确诊断的方式.

  1. 我将以肯定的方式回答这个问题(自动回答).
  2. 我想向您展示我在这个问题上找到的解决方案.
  3. 标准C99将被使用和尊重,以具有统一的背景.
  4. 但是(显然有一个"但是"),它将"定义"人们必须"吃"的某种"语法".
  5. 这种特殊语法旨在最简单地编写,最容易理解和/或处理,最大限度地降低生成错误的程序的风险,更重要的是,从编译器获取正确的诊断消息.
  6. 最后,它将研究两种情况:"非返回值"宏(简单情况)和"返回值"宏(不容易,但更有趣的情况).

让我们快速记住宏生成的一些典型陷阱.

例1

#define SQUARE(X) X*X
int i = SQUARE(1+5);
Run Code Online (Sandbox Code Playgroud)

预期值i:36.真值i:11(宏观扩展:) 1+5*1+5.陷阱!

(典型)解决方案(例2)

#define SQUARE(X) (X)*(X)
int i = (int) SQUARE(3.9);
Run Code Online (Sandbox Code Playgroud)

预期价值i:15.真实价值i:11(宏观扩张后:(int) (3.9)*(3.9)).陷阱!

(典型)解决方案(例3)

#define SQUARE(X) ((X)*(X))
Run Code Online (Sandbox Code Playgroud)

它适用于整数和浮点数,但它很容易被破坏:

int x = 2;
int i = SQUARE(++x);
Run Code Online (Sandbox Code Playgroud)

预期值i:9(因为(2+1)*(2+1)...).真值i:12(宏扩展:((++x)*(++x)),给出3*4).陷阱!

在这里可以找到一种很好的宏检查方法:

但是我想要更多:某种界面或"标准"语法,以及(少量)易于记忆的规则.意图是"能够使用(不实现)宏"与尽可能类似的功能.这意味着:写得好的假函数.

为什么在某种程度上有趣?

我认为这是在C中实现的一个有趣的挑战.

它有用吗?

编辑:在标准C中无法定义嵌套函数.但是,有时候,人们更愿意能够定义inline嵌套在其他函数中的short()函数.因此,类似功能的原型宏可能会被考虑在内.

pab*_*977 7

这个答案分为4个部分:

  1. 块宏的建议解决方案.
  2. 该解决方案的简短摘要.
  3. 讨论了宏原型语法.
  4. 为类功能宏提出的解决方案.
  5. (重要更新:)经纪我的代码.

(1.)第1个案例.块宏(或非返回值宏)

让我们先考虑简单的例子.假设我们需要一个"命令"来打印整数平方,然后是'\n'.我们决定用宏来实现它.但是我们希望编译器将参数验证为int.我们写:

#define PRINTINT_SQUARE(X) {    \
   int x = (X);              \
   printf("%d\n", x*x);      \
}
Run Code Online (Sandbox Code Playgroud)
  • 周围的括号(X)几乎避免了所有的陷阱.
  • 此外,括号有助于编译器正确诊断语法错误.
  • 宏参数X仅在宏内部调用一次.这避免了问题的示例3的陷阱.
  • X立即保存在变量中x.
  • 在宏的其余部分,我们使用变量x来代替X.
  • [重要更新:](此代码可以破解:请参阅第5节).

如果我们将这个学科系统化,将避免宏的典型问题.
现在,这样的东西正确打印9:

int i = 3;
PRINTINT_SQUARE(i++);  
Run Code Online (Sandbox Code Playgroud)

显然,这种方法可能有一个弱点:x宏内部定义的变量可能与程序中的其他变量发生冲突x.这是一个范围问题.但是,这不是一个问题,因为宏体已被写成一个封闭的块{ }.这足以处理每个范围问题,并且解决了"内部"变量的每个潜在问题x.

可以认为变量x是一个额外的对象,可能并不需要.但是x(只有)临时持续时间:它是在宏的开头创建的,带有开头{,并且在宏的末尾被销毁,并且关闭}.以这种方式,x它作为一个函数参数工作:创建一个时间变量来保存参数的值,当宏"返回"时它最终被丢弃.我们没有犯任何功能还没有完成的罪!

更重要的是:当程序员试图用一个错误的参数"调用"宏时,编译器会给出一个函数在相同情况下给出的相同诊断.

所以,似乎每个宏观陷阱都已经解决了!

但是,我们有一个小的语法问题,你可以在这里看到:

因此,必须(我说)向do {} while(0)块状宏定义中添加一个构造:

#define PRINTINT_SQUARE(X) do {    \
   int x = (X);              \
   printf("%d\n", x*x);      \
} while(0)
Run Code Online (Sandbox Code Playgroud)

现在,这个do { } while(0)东西工作正常,但它是反美学的.问题是它对程序员没有直观的意义.我建议使用一种有意义的方法,如下所示:

#define xxbeg_macroblock do {
#define xxend_macroblock } while(0)
#define PRINTINT_SQUARE(X)        \
  xxbeg_macroblock             \
       int x = (X);            \
       printf("%d\n", x*x);    \
  xxend_macroblock
Run Code Online (Sandbox Code Playgroud)

(列入}xxend_macroblock避免一些模糊带while(0)).当然,这种语法不再安全.必须仔细记录以避免误用.考虑以下丑陋的例子:

{ xxend_macroblock printf("Hello");
Run Code Online (Sandbox Code Playgroud)

(2.)总结

如果我们按照规范样式编写它们,那么不返回值的块定义宏可以像函数一样运行:

#define xxbeg_macroblock do {
#define xxend_macroblock } while(0)

#define MY_BLOCK_MACRO(Par1, Par2, ..., ParN)     \
  xxbeg_macroblock                         \
       desired_type1 temp_var1 = (Par1);   \
       desired_type2 temp_var2 = (Par2);   \
       /*   ...        ...         ...  */ \
       desired_typeN temp_varN = (ParN);   \
       /* (do stuff with objects temp_var1, ..., temp_varN); */ \
  xxend_macroblock
Run Code Online (Sandbox Code Playgroud)
  • 对宏的调用MY_BLOCK_MACRO()是一个语句,而不是一个表达式:没有任何类型的"返回"值,甚至没有void.
  • 宏参数必须在宏的开头只使用一次,并将它们的值传递给具有块范围的实际临时变量.在宏的其余部分中,只能使用这些变量.

(3.)我们可以为宏的参数提供一个接口吗?

虽然我们解决了参数类型检查的问题,但程序员无法弄清楚参数"有"的类型.有必要提供某种宏原型!这是可能的,而且非常安全,但我们必须容忍一些棘手的语法和一些限制.

你能弄明白以下几行吗?

xxMacroPrototype(PrintData, int x; float y; char *z; int n; );
#define PrintData(X, Y, Z, N) { \
    PrintData data = { .x = (X), .y = (Y), .z = (Z), .n = (N) }; \
    printf("%d %g %s %d\n", data.x, data.y, data.z, data.n); \
  }
PrintData(1, 3.14, "Hello", 4);
Run Code Online (Sandbox Code Playgroud)
  • 第一行"定义" 宏的原型PrintData.
  • 下面,声明了类似函数的宏PrintData.
  • 第3行声明了一个时间变量data,它立即收集宏的所有参数.
  • 这个步骤需要由程序员手动编写......但它是一种简单的语法,并且编译器拒绝(至少)分配给具有错误类型的临时变量的参数.
  • (但是,编译器将对"反向"赋值保持沉默.x = (N), .n = (X)).

要声明原型,我们xxMacroPrototype用2个参数写:

  1. 宏的名称.
  2. 将在宏内部使用的"本地"变量的类型和名称列表.我们将调用此项:宏的伪参数.

    • 伪参数列表必须写为类型变量对列表,用分号(;)分隔(和结束).

    • 在宏的主体中,第一个语句将是此形式的声明:
      MacroName foo = { .pseudoparam1 = (MacroPar1), .pseudoparam2 = (MacroPar2), ..., .pseudoparamN = (MacroParN) }

    • 在宏内部,伪参数被调用为foo.pesudoparam1,foo.pseudoparam2等等.

xxMacroPrototype()的定义如下:

#define xxMacroPrototype(NAME, ARGS) typedef struct { ARGS } NAME
Run Code Online (Sandbox Code Playgroud)

简单,不是吗?

  • 伪参数实现为typedef struct.
  • 保证ARGS是一个构造良好的类型 - 标识符对的列表.
  • 保证编译器将提供可理解的诊断.
  • 伪参数列表与struct声明具有相同的限制.(例如,可变大小的数组只能位于列表的末尾).(特别是,建议使用指针代替可变大小的数组声明符作为伪参数.)
  • 不保证NAME是真正的宏名称(但这个事实并不太相关).
    重要的是我们知道某些struct-type已定义为"there",与宏的参数列表相关联.
  • 不能保证ARGS提供的伪参数列表实际上与真实宏的参数列表在某种程度上重合.
  • 程序员不能保证在宏内正确使用它.
  • struct-type声明的范围xxMacroPrototype调用完成的点相同.
  • 建议练习将宏原型紧接在一起,然后是相应的宏定义.

但是,使用这种声明很容易受到纪律处分,程序员很容易遵守规则.

块宏可以"返回"一个值吗?

是.实际上,它可以通过简单地通过引用传递参数来检索任意数量的值,就像scanf()这样.

但你可能正在考虑其他事情:

(4.)第二种情况.类似函数的宏

对于他们来说,我们需要一个不同的方法来声明宏原型,其中包含返回值的类型.此外,我们必须学习一种(非硬)技术,让我们保持块宏的安全性,返回值具有我们想要的类型.

可以实现参数的类型检查,如下所示:

在块宏中,我们可以NAME在宏本身内部声明struct变量,
从而将其隐藏到程序的其余部分.对于类似函数的宏,这是不可能的(在标准C99中).我们必须NAME在调用宏之前定义一个类型的变量.如果我们准备支付这个价格,那么我们可以获得所需的"安全功能宏",并返回特定类型的值.
我们用一个例子展示代码,然后我们对它进行评论:

#define xxFuncMacroPrototype(RETTYPE, MACRODATA, ARGS) typedef struct { RETTYPE xxmacro__ret__; ARGS } MACRODATA

xxFuncMacroPrototype(float, xxSUM_data, int x; float y; );
xxSUM_data xxsum;
#define SUM(X, Y) ( xxsum = (xxSUM_data){ .x = (X), .y = (Y) }, \
    xxsum.xxmacro__ret__ = xxsum.x + xxsum.y, \
    xxsum.xxmacro__ret__)

printf("%g\n", SUM(1, 2.2));
Run Code Online (Sandbox Code Playgroud)

第一行定义了函数宏原型的"语法".
这样的原型有3个参数:

  1. "返回"值的类型.
  2. 用于保存伪参数的"typedef结构"的名称.
  3. 伪参数列表,以分号(;)分隔(和结束).

"return"值是结构中的附加字段,具有固定名称:xxmacro__ret__.
为安全起见,这是结构中的第一个元素.然后"粘贴"伪参数列表.

当我们使用这个界面时(如果你让我这样称呼它),我们必须遵循一系列规则,按顺序:

  1. 编写一个原型声明,为xxFuncMacroPrototype()提供3个参数(示例的第2行).
  2. 第二个参数是typedef struct宏本身构建的名称,因此您不必担心,只需使用它(在此类型的示例中xxSUM_data).
  3. 定义一个类型只是struct-type的变量(在示例中:) xxSUM_data xxsum;.
  4. 使用适当数量的参数定义所需的:#define SUM(X, Y).
  5. 宏的主体必须用括号括起来( ),以获得EXPRESSION(因此,"返回"值).
  6. 在这个括号内,我们可以使用逗号运算符(,)分隔一长串的操作和函数调用.
  7. 我们需要的第一个操作是将宏SUM(X,Y)的参数X,Y"传递"给全局变量xxsum.这是通过:

xxsum = (xxSUM_data){ .x = (X), .y = (Y) },

观察 C99语法提供的复合文字的帮助下,在空中xxSUM_data创建类型的对象.为了安全起见,该对象的字段通过读取宏的参数X,Y来填充,只需一次,并用括号括起来. 然后我们评估一个表达式和函数列表,所有这些都用逗号运算符(,)分隔. 最后,在最后一个逗号之后,我们只写,这被认为是逗号表达式中的最后一个术语,因此是宏的"返回"值.

xxsum.xxmacro__ret__

为什么那么多东西?为什么typedef struct?使用结构比使用单个变量更好,因为信息被打包在一个对象中,并且数据对程序的其余部分保持隐藏.我们不想定义"很多变量"来保存程序中每个宏的参数.相反,通过系统地定义typedef struct与宏相关联,我们可以更容易地处理这样的宏.

我们可以避免上面的"外部变量"xxsum吗?由于复合文字是左值,人们可以相信这是可能的.
实际上,我们可以定义这种宏,如下所示:

但实际上,我找不到以安全的方式实现它的方法.
例如,上面的宏SUM(X,Y)不能仅使用此方法实现.
(我尝试用指针结构+复合文字制作一些技巧,但似乎不可能).

更新:

(5.)破坏我的代码.

第1节中给出的例子可以这样打破(正如Chris Dodd在下面的评论中给我看到的):

int x = 5;          /* x defined outside the macro */
PRINTINT_SQUARE(x);
Run Code Online (Sandbox Code Playgroud)

因为在宏内部还有另一个名为x的对象(this:,int x = (X);其中X是宏的形式参数PRINTINT_SQUARE(X)),实际上"传递"的是因为参数不是在宏外定义的"值"5,而是另一个:垃圾值.
要理解它,让我们在宏扩展后展开上面的两行:

int x = 5;
{ int x = (x); printf("%d", x*x); }
Run Code Online (Sandbox Code Playgroud)

x块内的变量被初始化...到它自己未确定的值!
通常,块宏的第1节到第3节中开发的技术可以以类似的方式打破,而我们用来保存参数的struct对象在块内声明.

这表明这种代码可以被破坏,所以它是不安全的:

不要试图在宏"内"声明"本地"变量来保存参数.

  • 有解决方案吗?我回答"是":我认为,为了避免在块宏的情况下出现这个问题(如第1节至第3节所述),我们必须重复我们为类似函数的宏所做的事情,即:声明宏之外的hold-parameters结构,紧跟xxMacroPrototype()在行之后.

这不那么雄心勃勃,但无论如何它回答了这个问题:"有多少可能......?".另一方面,现在我们对两种情况采用相同的方法:块和类函数宏.