C/C++ 中的可破坏命名作用域

Break-able named scopes in C/C++

本文关键字:作用域 可破坏 C++      更新时间:2023-10-16

简介

这个问题由此而来:命名循环成语:危险?对于不想阅读原始问题的人来说,这是关于做这样的事情:

named(label1) for(int i = 0 ; i < 10 ; i++) {
    for(int j = 0 ; j < 10 ; j++) {
      if(some_condition)
            break(label1); // exit outer loop
      }
}

这个新问题是关于"命名循环"习语的改进版本。如果你懒得阅读整篇文章,你可以直接转到这篇文章的"示例"部分,清楚地理解我在说什么。

设计缺陷

不幸的是,这个问题很快就结束了(后来又重新打开了),因为它更像是一个利弊辩论,而不是一个纯粹的技术问题。似乎它不符合SO问答格式。此外,我提供的代码有几个缺陷:

  • 关键字 break 由宏重新定义
  • 宏是用小写字母编写的
  • 它使一些可怕的东西可以编译(至少使用 MSVC):

    int foo() {
       named(label1) for(int i = 0 ; i < 10; i++)
       {
          if(some_condition)
          {
              break(label1);  // here it's ok, the behavior is obvious
          }   
       }
       break(label1); // it compiles fine without warning... but the behavior is pretty obscur!
    }
    
  • 它可能会破坏一些好看的代码。例如,由于范围问题,以下内容未编译。

    int foo() {
    named(label1)  for(int i = 0 ; i < 10 ; i++)
           named(label2)  for(int j = 0 ; j < 10 ; j++)
            if(i*j<15)
                cout << i*j << endl;
            else
                break(label2);
     }
    

更安全的实施

我试图解决所有这些问题,以获得命名循环的安全版本。更一般地说,它也可以称为可破解作用域,因为它可用于过早退出任何作用域,而不仅仅是循环。

以下是两个宏NAMEDBREAK的定义:

    #define NAMED(bn)   if(const bool _YOU_CANNOT_USE_NAMED_BREAK_IF_YOU_ARE_OUTSIDE_##bn##_ = true)
                            goto _named_loop_identifier__##bn##_;
                        else
                            _break_the_loop_labelled_##bn##_:
                            if(true)
                                {}
                            else
                                if(! _YOU_CANNOT_USE_NAMED_BREAK_IF_YOU_ARE_OUTSIDE_##bn##_)
                                    goto _break_the_loop_labelled_##bn##_;
                                else
                                    _named_loop_identifier__##bn##_:

    #define BREAK(bn) if(!_YOU_CANNOT_USE_NAMED_BREAK_IF_YOU_ARE_OUTSIDE_##bn##_){} 
                        else goto _break_the_loop_labelled_##bn##_

它看起来丑陋而繁琐,因为它也避免了MSVC或GCC可能生成的一些警告,如"未使用的变量","未引用的标签"或"建议明确的大括号"。此外,如果使用不当,它不应该编译,在这种情况下,错误消息将是可以理解的。例如:

    NAMED(loop1) for(int i = 0 ; i < 10; i++) {
        NAMED(loop2) for(int j = 0 ; j < i ; j++) {
            cout << i << "," << j << endl;
            if(j == 5) {
                BREAK(loop1);   // this one is okay, we exit the outer loop
            }
        }
        BREAK(loop2); // we're outside loop2, this is an error
    }

前面的代码将无法编译,并且编译器错误消息为第二个BREAK:"_YOU_CANNOT_USE_NAMED_BREAK_IF_YOU_ARE_OUTSIDE_loop2_",这是非常明确的。

在提出我的问题之前,我提供了两个例子来说明这些结构的(相对)有用性:

break外循环:

     NAMED(myloop) for(int i = 0 ; i < rows; i++) {
         for(int j = 0 ; j < cols ; j++) {
            if(some_condition) {
                 BREAK(myloop);
            }
         }
     }

退出特定范围:

NAMED(myscope1) {
    cout<< "a";
    NAMED(myscope2)
    {
        cout << "b";
        NAMED(myscope3)
        {
            cout << "c";
            BREAK(myscope2);
            cout << "d";
        }
        cout << "e";
    }
    cout <<"f";
}

此代码打印:abcf

我的问题

在定义我

的问题是什么之前,我必须定义它不是什么,因为我不想看到我的主题在 10 分钟内关闭。

不是"这是一个好主意吗?",也不是"你怎么看?"甚至"它有用吗?",因为stackoverflow似乎不是一个讨论的地方。无论如何,我已经知道答案:"Macro是邪恶的","Goto是邪恶的"和"改用lambdas"。

相反,我的问题是关于技术正确性和针对编程错误的鲁棒性。我希望这个结构尽可能安全。

  • 用户是否有可能滥用它并且仍然能够编译?我试图解决原始实现的明显问题,但C++非常复杂,也许我错过了一些东西?

  • 它会默默地破坏一些好看的代码吗?这是我主要关心的问题。它会干扰其他C++功能(异常处理、析构函数调用、其他条件语句或其他任何功能)吗?

我的目标是证明这种结构本质上并不危险。我已经知道在实际代码中使用它是一个非常糟糕的主意,因为其他程序员可能不清楚它,但是在个人项目中使用它是否足够安全?

编辑:布尔变量现在const(感谢Jens Gustedt)。稍后我将尝试将 if s 替换为 for s,以检查它在像这样使用时是否可以删除虚假警告:

if(true)
     BREAK(label);

编辑2:正如JensGustedt所注意到的,if语句中的变量声明在C中是不允许的(仅限C++)。用循环替换它的另一个原因。

我想你已经测试过了,但是......您在 NAMED(bn) 中声明的 bool 变量是否仍然可用 如果声明它的语句存在于同一块中?(:除非是这样,否则你的成语是行不通的。 :)

我可以看到这是安全的:

{
    NAMED(one) { ... }
}
BREAK(one);

编译器会在BREAK(nb);行上大惊小怪

但这看起来仍然不安全:

{
    NAMED(bn) { ... }
    BREAK(bn);
}

不安全的意义上说,编译器可能仍然接受定义的变量而不是大惊小怪。 但它可能会形成一个无限循环,从而悄无声息地破坏你的程序。

-耶西

PS:它不会干扰try..finally,因为无论您如何退出try块,finally块都被定义为执行。 因此,只要您不试图避免finally块,您就可以。

P(PS)S:我看到的与其他结构的唯一真正奇怪的交互是这样的:

#if DEBUG
    NAMED(bn)
#endif
    while(true)
    {
        BREAK(BN);
    }

这是病态的!;-D 这将在调试中编译良好,但在发布中得到编译器错误。

我真的无法回答 c++ 方面,但由于您还将其标记为 C...

通常对这样的事情使用 if/else 结构并不是一个好主意,因为您总会找到一个善意的 C 编译器,它会为奇怪的else匹配和类似的事情找到警告。

我通常使用 P99 这样的for结构。他们避免悬空else(或关于它的虚假警告)。此外,for 是 C 中唯一可以按照您需要的方式放置局部变量的地方,它们不允许在ifwhile中,因为它们在C++中是

不允许的。

在"安全"方面,您可能应该声明变量register bool const而不仅仅是bool。因此,没有人可以尝试更改它,甚至没有人在背后更改它的地址。

但是出于您使用它的特定目的,我也不太喜欢forgoto。您真正在做的是展开堆栈。在 C 中,有一个结构可以正确地做到这一点,即 setjmp/longjmp 。在C++,它可能是try/catch.