在C中自定义控制结构

在C中元编程自定义控制结构

Source

这篇文章是关于使用C的预处理器来在C中实现一种形式的元编程的技术, 让编程者可以自定
义语法类似C自带的for,while,if, 但是以一种自定义的方式管理控制流的控制结构.
这个技术差不多是完全c89兼容的, 除了一些构造需要在for语句初始化语句中定义变量的
特性(C99和C++中都有). 提供了简单的代码.

动机

C中已有的控制结构不需要介绍了. 每个C程序员都很熟悉if,while,do...while,以及
for语句, 以及它们对控制流的影响. 很多时候, 都会碰到, 能够创建一个略微不同的控
制结构的话会更好的情况.

举个例子, 如果你在迭代一个循环的链表, 没有一个有明显特征的head元素, 你会发现
for在循环开始进行测试这一点非常不方便, 因为, 很明显的迭代这样一个列表的方式(在
测试语句中比较元素指针和链表头的指针)一次都不会执行(第一次比较就停止了). 因此你
可能会想要一个这样的for, 其在循环末尾进行其测试:

for_after (elephant *e = head; e != head; e = e->next)
    do_stuff_with(e);

或者, 更好的是, 在第一次迭代的时候有一次单独的测试的, 这样你可以检查看链表是不是
完全空的:

for_general (elephant *e = head; e != NULL; e != head; e = e->next)
    do_stuff_with(e);

There's also a lot of scope for program-specific control constructions, 如果你发
现你在一个特定的代码库某个特定的循环谢了很多次. 比如, 假设你有某种类型的用于提取
一组东西的API, 并且API要求许多配置和清理以及函数调用. 你可能会发现实际最你的这组
东西进行操作的, 哪怕是最简单的, 可能都是像这样的:

{
    hippo_retrieval_context *hc;
    hc = new_hippo_retrieval_context();
    while (hippo_available(hc)) {
        hippo *h = get_hippo(hc);
        printf("We have a hippo called %s\n", h->name);
        free_hippo(hc, h);
    }
    free_hippo_retrieval_context(hc);
}

并且, 你发现, 你需要迭代这些东西很多次, 然后你的程序会就会被这些模板一样的东西的
副本占满. 在这样的一个程序中你可能会感觉得到, 如果能将这些机械式地操作封装到你程
序头文件中一个宏会很nice, 然后你每次都只需要循环中需要改变的部分:

FOR_EACH_HIPPO (hippo *h)
    printf("We have a hippo called %s\n", h->name);

如果你这样做的话, 你可能会让break的处理也变得有意义. 在上面的例子中, 如果
实际处理一组东西的程序想要结束循环, 可能会需要确保在其break语句之前加上另一个
free_hippo调用, 因为在while循环末尾的那个会被break跳过. 如果你在创建一个自
定义的控制结构的话, 可能会想要让break有可配置的处理过程, 以便其可以处理这种情
况.

很容易想到, 如果你非常想要将这种类型的东西加入到你的语言, 你可以通过创建一套二级
预处理器, 让其在标准C预处理器之后运行, 并且足够了解C语法, 能够识别语句, 块, 声明
和表达式. 然后你就可以定义可以看起来很像附加语法规则的, 并通过他们实现你的额
外的控制结构.

但是, 实际上, 完全没必要那么做. 已经有在标准C中实现和上面的几乎一样的控制结构的
方法, 如果你已经准备好来瞎搞已有的C预处理器的话..在这个文章中, 我会展示, 并提供
一些示例代码.

(我说的是几乎, 因为, 由于C中宏的语法限制, 上面的样板中有一件事是做不到的, 即,
for_afterfor_general中使用分号分隔语句, 只能使用逗号)

机制

如果我们要自定义这种类型的循环控制的话, 我们如何能做到它?

如果我们想要我们最终的控制结构使用像下面这样的语法:

MY_LOOP_KEYWORD (parameters)
    statement or braced block

那么, 很明显, 我们需要将MY_LOOP_KEYWORD定义为一个宏, 并且宏 必须展开为一个语句
前缀: 也就是, 某些你可以放到一个合法的C语句之前, 来生成另一个合法的C语句的东西.(
整个文章就这个了吧)

因此, 在这些约束下我们能做什么呢? 呃, 在C的语法中有几种语句前缀:

  • label
  • while (stuff)
  • for (stuff; stuff; stuff)
  • if (stuff) {stuff} else
  • 上面这些一个接一个的组合...
    (还有switch (stuff), 以及case label, 但是我们最好尽可能避免这些, because of
    their side effect of interfering with case labels from an outer switch. 事实表明
    我们并不需要他们, 上面的列表就够了)

所以, 现在我们来探索定义展开为上面这些类型的东西的宏, 然后将其前缀到一个用户提供
的语句的可能性范围.

上面这个列表中最关键的部分是if..else语句前缀, 因为其提供了让我们提供自己的语句
块的能力, 我们可以在这个块中加入我们想要的代码. 这对需要在开始和结束的时候, 或者
在迭代之间, 运行特定的代码的循环, 是非常重要的.

就从这里开始吧, 这个是一个可以让我们允许我们提供的代码, 然后运行使用者的代码的构
造. 假设我们将宏定义为展开成这样:

if (1) {
    /* code of our choice */
    goto body;
} else
    body:
        { /* block following the expanded macro */ }

从顶部跟着控制流走, 很容易看出这是如何工作的. 我们进入到if语句, 然后条件总是为
真, 因此我们先执行'code of our choice'部分, 然后就到了goto, 这让我们很方便的就
进入到了同一个if语句的else从句.. 即使那里本来根本就不可能会执行. 这样, 我们
先执行了我们的代码, 然后是使用者的代码.

非常nice, 也非常简单. 那么, 如果我们想要在使用者的循环主体之后运行我们的代码怎么
办?

我们当然不能值使用if和lable, 因为这些无法阻止退出用户块之后继续执行之后的代码.
我们想要在之后运行的代码需要在用户的代码之上运行, 这意味着在源文件中控制流需要会
退 -- 为了实现这个, 我们需要使用一个循环结构. 因此我们可以像这样:

if (1)
    goto body;
else
    while (1)
        if (1) {
            /* code of our choice */
            break;
        } else
            body:
                { /* block following the expanded macro */ }

在前面, 我们先进入最外层if的then部分(就是不是else的那部分..), 其中包含一个跳转
到使用的块的goto. 我们执行那个块, 那么之后呢? 额, 那个块是位于一个while(1)
句之中的, 因此, 我们现在回到了while语句顶端, 进入了内层if的then部分, 在里面
我们可以运行我们的代码, 之后, 执行break, 结束循环. 这样, 我们成功地加入了在使
用者的代码执行之后执行的代码. 并且整个控制结构仍然是完全由前缀到使用者的块之前的
语句链组成, 我们可以定义一个展开为上面模板中除最后一行的宏.

前途一片光明. 我们可以使用上面的这些来让带阿妈在使用者的代码之前和之后运行, we
can futher mess about with the control flow by, 在各个地方加入label, 然后让我们
插入的代码块测试条件, 好好想想怎么做, 然后发起一个适当的goto

另一个可能想做的事是, 将代码声明加入作用域, 让其既可以被我们添加到if块中的语句访
问, 也可以被使用者的代码访问. 很遗憾这个在旧的C89中无法做到, 因为你只能通过{
符来创建新的作用域, 那意味着用于需要在他们的语句之后提供Ewanidentifier闭合括号,
这样看起来就很丑, 并且会影响编辑器的自动C缩进策略.

但是, 如果你允许你自己使用for语句的初始化声明的话, 那么你突然就可以将任何声明
加入到作用域了..

因此一个明显的解决办法是, 将声明置于for中, 然后使用和上面的完全一样的方法来让
for实际上并不循环: 也就是, 重复前面的代码模板, 只是将while改成for.

这并不怎么理想, 因为执行会跳过声明而不是经过它. 如果声明并没有包含初始化指定, 这
并没有区别, 但是你可能会想要一次指定声明和初始化. xxx. 一个原因是, 使用者给循环
宏提供了一个参数, 你想要将其作为一个组合的声明和初始化的一部分( 如果使用者提供了
形如type *var的宏参数, 我们可以将那个参数置于一个赋值之前来生成带初始化的生成,
但是我们无法从其中提取变量名来单独进行初始化.)

所以, 如何解决呢? 我们需要做两件事, 一个是在使用者的块运行之后恢复控制(但是??我
们可以使用上面的给予while的构造来实现), 另一个是找到一个将控制移出for的方式(
不要错误的在while的执行情境下使用break)

对于这个, 一个有用的招数是, 在将一个label加入到if(0)中, 像这样:

if (0)
    finished: ;
else
    for (/* declaration of our choice */ ;;)
        if (1)
            goto body;
        else
            while (1)
                if (1)
                    goto finished;
                else
                    body:
                        { /* block following the expanded macro */ }

因此, 最开始的if(0)被忽略, 然后我们开始执行for语句, 包括其声明. for语句中
的构造现在应该和前面的例子中的类似了: 他的工作是执行使用者的块, 然后转移控制到
goto finished语句, 其又转移控制到最外层if的then语句. 在哪里, 控制跳过整个
else从句, 然后直到底部. 因此声明的目的达到了

那么, 现在, 如何处理break呢? 你可以想想上面的代码片段中后缀的块中执行了break
会怎样, 你会发现第一个(在使用者代码之前执行'我们要的代码'的那个)完全不受break
影响., 因此其仍然会结束写一个最外层loop或者switch, 但是另外的两个, 其中使用者的
代码是嵌入在一个while或者一个for中的, 都会改变break的意义, 其会结束while
或者for. 如果我们想要将这个结构嵌入到一个实际的循环(我指的是, 多次执行使用者的
代码, 而不像上面的这些总是在一次迭代之后就退出的伪循环)中的话, 会非常恶心人, 因
为这样的话, 使用者的代码中的break不会结束实际的循环, 只会结束当前的循环体, 换
句话说, 它会变成continue的同义词, 这并没什么用.

第一眼看上去的话, 可能会觉得, 既然问题是因为使用C循环关键字导致的, 除了移除循环
并没有别的办法. 但是, 实际上, 我们可以往另一个角度想: 我们可以通过添加另一层循环
来恢复有效的break处理. 特别的说, 我们将使用者的代码加入到两个嵌套的while循环
中, 像这样:

if (1)
    goto body;
else
    while (1)
        if (1) {
            /* we reach here if the block terminated by break */
        } else
            while (1)
                if (1) {
                    /* we reach here if the block terminated normally */
                } else
                    body:
                        { /* block following the expanded macro */ }

就像前面的例子一样, 最外层的if跳转到最内层的循环, 然后执行使用者的代码. 如果那
个块正常退出, 那么我们再次循环, 到内层while的顶端, 然后执行我们的代码片段. 然
是如果使用者的块由于break语句结束了的话, 就会结束内层while, 但是我们循环到另
一个while的顶部. 现在我们可以根据使用者是否使用了break来调整控制流的走向, 让
然, 上面我们提供的每一个代码片段都可以根据那个信息来做出响应, 包括通过goto跳转
到一个特定的位置

posted on 2019-12-09 23:48  jakio6  阅读(128)  评论(0编辑  收藏  举报