在 C 中使用宏创建伪函数的可能性有多大? [英] How much is it possible to create fake-functions with macros in C?

查看:17
本文介绍了在 C 中使用宏创建伪函数的可能性有多大?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

People always say that macros are unsafe, and also that they are not (directly) type-checking on their arguments, and so on. Worse: when errors occur, the compiler gives intrincate and incomprehensible diagnostics, because the macro is just a mess.

是否有可能以与函数几乎相同的方式使用宏,通过进行安全的类型检查,避免典型的陷阱,并且以某种方式编译器给出正确诊断.

Is it possible to use macros in almost the same way as a function, by having safe type-checking, avoiding typical pitfalls and in a way that the compiler gives the right diagnostic.

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

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

Let us quickly remember some typical pitfalls produced by macros.

示例 1

#define SQUARE(X) X*X
int i = SQUARE(1+5);

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

Intended value of i: 36. True value of i: 11 (with macro expansion: 1+5*1+5). Pitfall!

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

#define SQUARE(X) (X)*(X)
int i = (int) SQUARE(3.9);

i 的预期值:15.i 的真值:11(宏展开后:(int) (3.9)*(3.9)).陷阱!

Intended value of i: 15. True value of i: 11 (after macro expansion: (int) (3.9)*(3.9)). Pitfall!

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

#define SQUARE(X) ((X)*(X))

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

It works fine with integers and floats, but it is easily broken:

int x = 2;
int i = SQUARE(++x);

i 的预期值:9(因为 (2+1)*(2+1)...).i的真值:12(宏展开:((++x)*(++x)),得到3*4).陷阱!

Intended value of i: 9 (because (2+1)*(2+1)...). True value of i: 12 (macro expansion: ((++x)*(++x)), which gives 3*4). Pitfall!

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

A nice method for type-checking in macros can be found here:

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

However I want more: some kind of interface or "standard" syntax, and a (small) number of easy-to-remember rules. The intent is "be able to use (not to implement)" macros as similar to functions as possible. That means: well written fake-functions.

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

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

I think that is an interesting challenge to achieve in C.

有用吗?

在标准 C 中无法定义嵌套函数.但是,有时,人们希望能够定义嵌套在其他函数中的短 (inline) 函数.因此,可以考虑使用类似函数的原型宏.

In standard C is not possible to define nested functions. But, sometimes, one would prefer to be able to define short (inline) functions nested inside other ones. Thus, a function-like prototyped macro would be a possibility to take in account.

推荐答案

这个答案分为4个部分:

This answer is divided in 4 sections:

  1. 针对块宏提出的解决方案.
  2. 对该解决方案的简要总结.
  3. 讨论了宏原型语法.
  4. 针对类似函数的宏提出的解决方案.
  5. (重要更新:)破坏我的代码.
  1. Proposed solution for block macros.
  2. A brief summary of that solution.
  3. Macro-prototype syntax is discussed.
  4. Proposed solution for function-like macros.
  5. (Important update:) Broking my code.

(1.) 第一种情况.块宏(或非返回值宏)

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

Let us consider easy examples first. Suppose that we need a "command" that prints the square of integer numbers, followed by ' '. We decided to implement it with a macro. But we want the argument to be verified by the compiler as an int. We write:

#define PRINTINT_SQUARE(X) {    
   int x = (X);              
   printf("%d
", x*x);      
}

  • (X) 周围的括号避免了几乎所有的陷阱.
  • 此外,括号有助于编译器正确诊断语法错误.
  • 宏参数X 只在宏内部调用一次.这避免了问题示例 3 的陷阱.
  • X 的值立即保存在变量x 中.
  • 在宏的其余部分,我们使用变量 x 代替 X.
  • [重要更新:](此代码可能会被破坏:请参阅第 5 部分).
    • The parentheses surrounding (X) avoid almost all pitfalls.
    • Moreover, the parentheses help the compiler to properly diagnose syntax errors.
    • The macro parameter X is invoked only once inside the macro. This avoids the pitfall of Example 3 of the question.
    • The value of X is immediately held in the variable x.
    • In the rest of the macro, we use the variable x instead X.
    • [Important Update:] (This code can be broken: see section 5).
    • 如果我们把这门学科系统化,就可以避免宏的典型问题.
      现在,像这样正确打印 9:

      If we systematize this discipline, the typical problems of macros will be avoided.
      Now, something like this correctly prints 9:

      int i = 3;
      PRINTINT_SQUARE(i++);  
      

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

      Obviously this approach could have a weak point: the variable x defined inside the macro could have conflicts with other variables in the program also called x. This is a scope issue. However, it's not a problem since the macro-body has been written as a block enclosed by { }. This is enough to handle every scope-issue, and every potential problem with the "inner" variables x is tackled.

      可以说变量 x 是一个额外的对象,可能不是我们想要的.但是 x 有(仅)临时持续时间:它在宏开始时创建,以 { 开头,在宏结束时被销毁,以结束 }.这样, x 它就作为函数参数工作:创建一个临时变量来保存参数的值,并在宏返回"时最终将其丢弃.我们没有犯任何功能尚未完成的罪!

      It could be argued that the variable x is an extra object and maybe not desired. But x has (only) temporary duration: it is created at the beginning of the macro, with the opening {, and it is destroyed at the end of the macro, with the closing }. In this way, x it is working as a function parameter: a temporal variable is created to hold the value of the parameter, and it is finally discarded when the macro "returns". We are not committing any sin that functions have not done yet!

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

      More important: when the programmer attempts to "call" the macro with a wrong parameter, the compiler gives the same diagnostic that a function would give under the same situation.

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

      So, it seems every macro pitfall has been solved!

      但是,我们有一点语法问题,您可以在此处看到:

      However, we have a little syntactical issue, as you can see here:

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

      Therefore, it is imperative (I say) to add a do {} while(0) construct to the block-like macro definition:

      #define PRINTINT_SQUARE(X) do {    
         int x = (X);              
         printf("%d
      ", x*x);      
      } while(0)
      

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

      Now, this do { } while(0) stuff works fine, but it is anti-aesthetical. The problem is that it has no intuitive meaning for the programmer. I suggest the use of a meaningful approach, like this:

      #define xxbeg_macroblock do {
      #define xxend_macroblock } while(0)
      #define PRINTINT_SQUARE(X)        
        xxbeg_macroblock             
             int x = (X);            
             printf("%d
      ", x*x);    
        xxend_macroblock
      

      (在 xxend_macroblock 中包含 } 避免了与 while(0) 的一些歧义).当然,这种语法不再安全了.必须仔细记录以避免误用.考虑以下丑陋的例子:

      (The inclusion of } in xxend_macroblock avoids some ambiguity with while(0)). Of course, this syntax is not safe anymore. It has to be carefully documented to avoid misuses. Consider the following ugly example:

      { xxend_macroblock printf("Hello");
      

      (2.) 总结

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

      Block-defined macros that do not return values can behave like functions if we write them by following the disciplined style:

      #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
      

      • 对宏MY_BLOCK_MACRO() 的调用是语句,而不是表达式:没有任何类型的返回"值,甚至不是 void.
      • 宏参数只能在宏开始处使用一次,并将它们的值传递给具有块作用域的实际临时变量.在宏的其余部分中,只能使用这些变量.
        • A call to the macro MY_BLOCK_MACRO() is a statement, not an expression: there is no "return" value of any kind, not even void.
        • The macro parameters must be used just once, at the beginning of the macro, and pass their values to actual temporary variables with block-scope. In the rest of the macro, only these variables may be used.
        • (3.) 能否提供宏的参数接口?

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

          Although we solved the problem of type-checking of parameters, the programmer cannot figure out what type the parameters "have". It is necessary to provide some kind of macro prototype! This is possible, and very safely, but we have to tolerate a little tricky syntax and some restrictions, also.

          你能弄清楚以下几行的作用吗?

          Can you figure out what the following lines do?

          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
          ", data.x, data.y, data.z, data.n); 
            }
          PrintData(1, 3.14, "Hello", 4);
          

          • 第一行定义"宏PrintData原型.
          • 下面,声明了类似函数的宏PrintData.
          • 第 3 行声明了一个临时变量 data,它一次性收集宏的所有参数.
          • 这一步需要程序员手动编写……但它是一种简单的语法,编译器会拒绝(至少)分配给类型错误的临时变量的参数.
          • (但是,编译器将对反向"赋值保持沉默.x = (N), .n = (X)).
            • The 1st line "defines" the prototype for the macro PrintData.
            • Below, the function-like macro PrintData is declared.
            • The 3rd line declares a temporal variable data which collects all the arguments of the macro, at once.
            • This step requires to be manually written with care by the programmer...but it is an easy syntax, and the compiler rejects (at least) the parameters assigned to temporary variables with the wrong type.
            • (However, the compiler will be silent about the "reversed" assignment .x = (N), .n = (X)).
            • 为了声明一个原型,我们用 2 个参数编写 xxMacroPrototype:

              To declare a prototype, we write xxMacroPrototype with 2 arguments:

              1. 宏的名称.
              2. 将在宏内部使用的局部"变量的类型和名称列表.我们将调用这个项目:宏的pseudoparameters.

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

              • The list of pseudoparameters has to be written as a list of type-variable pairs, separated (and ended) by semicolons (;).

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

              In the body of the macro, the first statement will be a declaration of this form:
              MacroName foo = { .pseudoparam1 = (MacroPar1), .pseudoparam2 = (MacroPar2), ..., .pseudoparamN = (MacroParN) }

              xxMacroPrototype()的定义如下:

              The definition of xxMacroPrototype() is as follows:

              #define xxMacroPrototype(NAME, ARGS) typedef struct { ARGS } NAME
              

              很简单,不是吗?

              • 伪参数被实现为 typedef struct.
              • 保证 ARGS 是一个结构良好的类型标识符对列表.
              • 保证编译器会给出易于理解的诊断.
              • 伪参数列表具有与 struct 声明相同的限制.(例如,可变大小的数组只能位于列表的末尾).(特别推荐使用 pointer-to 而不是 variable-size array 声明符作为伪参数.)
              • 不保证 NAME 是一个真正的宏名(但这个事实并不太相关).
                重要的是我们知道那里"定义了一些结构类型,与宏的参数列表相关联.
              • 不能保证 ARGS 提供的伪参数列表实际上在某种程度上与真实宏的参数列表一致.
              • 不保证程序员会在宏中正确使用它.
              • 结构类型声明的范围与完成xxMacroPrototype调用的点相同.
              • 建议将宏原型放在一起,紧接着是相应的宏定义.
              • The pseudoparameters are implemented as a typedef struct.
              • It is guaranteed that ARGS is a list of type-identifier pairs that is well constructed.
              • It is guaranteed that the compiler will give understandable diagnostics.
              • The list of pseudoparameters has the same restrictions than a struct declaration. (For example, variable-size arrays only can be at the end of the list). (In particular, it is recommended to use pointer-to instead of variable-size array declarators as pseudoparameters.)
              • It is not guaranteed that NAME is a real macro-name (but this fact is not too relevant).
                What matters is that we know that some struct-type has been defined "there", associated to the parameter-list of a macro.
              • It is not guaranteed that the list of pseudoparameters, provided by ARGS actually coincides in some way with the list of arguments of the real macro.
              • It is not guaranteed that a programmer will use this correctly inside the macro.
              • The scope of the struct-type declaration is the same as the point where the xxMacroPrototype invocation is done.
              • It is recommended practice to put together the macro prototype immediately followed by the corresponding macro definition.

              但是,使用这种声明很容易受到约束,程序员也很容易遵守规则.

              However, it is easy to be disciplined with that kind of declarations, and it is easy to the programmer to respect the rules.

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

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

              Yes. Actually, it can retrieve as many values as you want, by simply passing arguments by reference, as scanf() does.

              但你可能在想别的东西:

              But you probably are thinking of something else:

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

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

              For them, we need a little different method to declare macro-prototypes, one that includes a type for the returned value. Also, we'll have to learn a (not-hard) technique that let us to keep the safety of block-macros, with a return value having the type we want.

              参数的类型检查可以这样实现:

              The typechecking of arguments can be achieved as shown here:

              在块宏中,我们可以在宏本身内声明结构变量NAME
              从而使其对程序的其余部分隐藏.对于类似函数的宏,这是无法做到的(在标准 C99 中).我们必须在调用宏之前定义一个 NAME 类型的变量.如果我们准备好为此付出代价,那么我们可以获得所需的类似安全函数的宏",并返回特定类型的值.
              我们通过示例展示代码,然后对其进行注释:

              In block-macros we can declare the struct variable NAME just inside the macro itself,
              thus keeping it hidden to the rest of the program. For function-like macros this cannot be done (in standard C99). We have to define a variable of type NAME before any invocation of the macro. If we are ready to pay this price, then we can earn the desired "safe function-like macro", with returning values of a specific type.
              We show the code, with an example, and then we comment it:

              #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
              ", SUM(1, 2.2));
              

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

              The first line defines the "syntax" for function-macro prototypes.
              A such prototype has 3 arguments:

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

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

              The "return" value is an additional field in the struct, with a fixed name: xxmacro__ret__.
              This is declared, for safety, as the first element in the struct. Then the list of pseudoparameters is "pasted".

              当我们使用这个接口时(如果你让我这样称呼它),我们必须遵循一系列规则,依次:

              When we use this interface (if you let me call it this way), we have to follow a series of rules, in order:

              1. 编写一个原型声明,为 xxFuncMacroPrototype() 提供 3 个参数(示例的第 2 行).
              2. 第二个参数是宏自己构建的typedef struct的名称,不用担心,直接使用即可(本例中该类型为xxSUM_data).
              3. 定义一个变量,其类型就是该结构类型(在示例中:xxSUM_data xxsum;).
              4. 使用适当数量的参数定义所需的:#define SUM(X, Y).
              5. 宏的主体必须用括号 ( ) 括起来,以便获得一个 EXPRESSION(因此,一个返回"值).
              6. 在这个括号内,我们可以使用逗号运算符 (,) 分隔一长串操作和函数调用.
              7. 我们需要的第一个操作是将宏 SUM(X,Y) 的参数 X、Y传递"给全局变量 xxsum.这是由以下人员完成的:
              1. Write a prototype declaration giving 3 paramenters to xxFuncMacroPrototype() (the 2nd line of the example).
              2. The 2nd parameter is the name of a typedef struct that the macro itselfs builds, so you have not worry about, and just use it (in the example this type is xxSUM_data).
              3. Define a variable whose type is simply that struct-type (in the example: xxSUM_data xxsum;).
              4. Define the desired macro, with the appropriate number of arguments: #define SUM(X, Y).
              5. The body of the macro must be surrounded by parenthesis ( ), in order to obtain an EXPRESSION (thus, a "returning" value).
              6. Inside this parenthesis, we can separate a long list of operations and function calls by using comma operators (,).
              7. The first operation we need is to "pass" the arguments X, Y, of the macro SUM(X,Y), to the global variable xxsum. This is done by:

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

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

              Observe that an object of type xxSUM_data is created in the air with the aid of compound literals provided by C99 syntax. The fields of this object are filled by reading the arguments X, Y, of the macro, just once, and surrounded by parenthesis, for safety.
              Then we evaluate a list of expressions and functions, all of them separated by comma operators (,).
              Finally, after the last comma, we just write xxsum.xxmacro__ret__, which is considered as the last term in the comma expression, and thus is the "returning" value of the macro.

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

              Why all that stuff? Why a typedef struct? To use a struct is better than use individual variables, because the information is packed all in one object, and the data keep hidden to the rest of the program. We don't want to define "a lot of variables" to hold the arguments of each macro in the program. Instead, by defining systematically typedef struct associated to a macro, we have a more easy to handle such macros.

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

              Can we avoid the "external variable" xxsum above? Since compound literals are lvalues, one can believe that this is possible.
              In fact, we can define this kind of macros, as shown in:

              但在实践中,我找不到以安全方式实施它的方法.
              例如上面的宏 SUM(X,Y) 不能只用这个方法来实现.
              (我试图用指向结构的指针 + 复合文字来做一些技巧,但这似乎是不可能的).

              But in practice, I cannot find the way to implement it in a safe way.
              For example, the macro SUM(X,Y) above cannot be implemented with this method only.
              (I tried to make some tricks with pointer-to-struct + compound literals, but it seems impossible).

              更新:

              (5.) 破坏我的代码.

              第 1 节中给出的示例可以这样分解(正如 Chris Dodd 在下面的评论中向我展示的那样):

              The example given in Section 1 can be broken this way (as Chris Dodd showed me in his comment, below):

              int x = 5;          /* x defined outside the macro */
              PRINTINT_SQUARE(x);
              

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

              Since inside the macro there is another object named x (this: int x = (X);, where X is the formal parameter of the macro PRINTINT_SQUARE(X)), what is actually "passed" as argument is not the "value" 5 defined outside the macro, but another one: a garbage value.
              To understand it, let us unroll the two lines above after macro expansion:

              int x = 5;
              { int x = (x); printf("%d", x*x); }
              

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

              The variable x inside the block is initialized... to its own undetermined value!
              In general, the technique developed in sections 1 to 3 for block macros can be broken in a similar way, while the struct object we use to hold the parameters is declared inside the block.

              这说明这种代码是可以破解的,所以是不安全的:

              This shows that this kind of code can be broken, so it is unsafe:

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

              Don't try to declare "local" variables "inside" the macro to hold the parameters.

              • 有解决方案"吗?我回答是":我认为,为了避免在块宏的情况下出现这个问题(如第 1 到第 3 节所述),我们必须重复我们对类函数宏所做的事情,即:声明宏外部的holding-parameters 结构,就在xxMacroPrototype() 行之后.
              • 这不那么雄心勃勃,但无论如何它回答了这个问题:有多少可能......?".另一方面,现在我们对两种情况采用相同的方法:块宏和类函数宏.

                This is less ambitious, but anyway it responses the question: "How much is it possible to...?". On the other hand, now we follow the same approach for the two cases: block and function-like macros.

                这篇关于在 C 中使用宏创建伪函数的可能性有多大?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆