Is it possible to ban modifying loop variables inside the body of for-loops?
在 C 或 C 中,修改 for 循环内的循环变量是令人讨厌的错误的来源:
1
2 3 4 5 6 7 8 9 10 11 12 |
int main() {
std::vector<int> v (30); std::iota(v.begin(), v.end(), 0); int j = 0; for (size_t i = 0; i < v.size(); i++) { std::cout << v[i] << ‘ ‘ << i << ‘\ ‘; i++; // oops, I mean j++ } std::cout << j << ‘\ ‘; } |
有什么方法可以禁止或警告在编译器或其他东西的帮助下修改循环体内的循环变量?如果可能的话,我该怎么做?
- 我建议更好的命名。 i 和 j 不是很具体,这就是为什么您最终在示例中修改 i 而不是 j 的原因。
- 在其他一些语言中,这可以通过一种特殊类型的迭代器来完成,它返回元素及其索引的元组,但我认为在 C 中这样做是一种反模式。也许在 C 中可行。
- @TedLyngmo:这个问题明确询问了 C 和 C 。示例代码是 C 并不否定这一点。当他们真正只处理一个时,发帖人通常会同时标记 C 和 C,但这个问题对两者都进行询问并非没有道理。
- 回覆。返回元素及其索引的迭代器,请参阅:stackoverflow.com/questions/24881799/…(特定于 C)
- 可能会有所帮助:stackoverflow.com/a/10962575/4342498
- @EricPostpischil 好的,但是问两个单独的问题不是更清楚吗?或者更多,如果一个人也想投入 Java 等等。
- 免责声明:到目前为止,我没有对任何答案投反对票。但是,这些解决方案有效,但看起来并不整洁。
- @frozenca 您正在寻找的东西并不是应该以”整洁”的方式解决的东西。不能真正责怪我们的答案:\\\\对不起,有人认为那些人没有回答这个问题。
- 是的,我正在寻找的只是是否有一个编译器选项会发出警告。如果没有,也许我应该更仔细地编码..
- 我明白了,我更新了我的答案。
- 能够操纵循环变量有时是一项重要功能。例如,当您需要从集合中删除一些条目时。经典的 C 方法是用数组中的最后一个条目覆盖已删除的条目,并减少循环边界和循环计数器以在下一次迭代中重新考虑移动的条目。
- 通常,这将使用外部 MISRA-C 检查器来完成。它是一个静态分析工具,用于检查 MISRA 合规性,并且 MISRA 禁止您从 for 循环体内修改迭代器等。
- @Lundin,我也想知道类似 MISRA 的检查器是否没有起到作用。谢谢确认。绝对应该是答案。
如果你使用 C 的 ranged-for,你可以使循环变量 const。例如
1
2 3 4 5 6 |
for (const size_t i : boost::irange<size_t>(0, v.size()))
{ std::cout << v[i] << ‘ ‘ << i << ‘\ ‘; // i++; // error, can’t modify const } |
对于 C,你可以创建一个索引类来使用。因此,以下内容将是一个起点。我确信它可以改进,因为我没有考虑太多。
1
2 3 4 5 6 7 8 9 10 11 12 13 |
class CIndex {
private: size_t m_index; public: CIndex(size_t i=0) : m_index(i) {} ~CIndex() {}; size_t inc(void) { return ++m_index; } bool operator < (size_t i) { return m_index < i; } |
并且它会被用作:
1
2 3 4 |
for (CIndex x; x < 10; x.inc()) {
std::cout << argv[x.val()]; x = 3; // generates an error with Visual Studio 2017 } |
您可以使用转换运算符修改上述类,使其更直观并类似于标准 size_t 变量。还要添加一个减量运算符。由于想法是使用它代替 size_t 您不再需要比较运算符,因为编译器将进行转换并使用内置比较循环结束。您可能还希望能够指定可选的增量或减量。
修改后的类看起来像:
1
2 3 4 5 6 7 8 9 10 11 12 13 |
class CIndex {
private: size_t m_index; public: CIndex(size_t i = 0) : m_index(i) {} ~CIndex() {}; size_t inc(size_t i = 1) { return (m_index += i); } // increment operator CIndex & operator =(size_t i) = delete; |
这将允许您在几乎可以使用 size_t 的任何地方使用 CIndex。所以数组索引可以写成 std::cout << argv[x]; 而不是 std::cout << argv[x.val()];.
但是对于 C 语言规范,这并没有允许您将变量标记为在特定范围内不可变或不变。
您真正要求的是能够标记特定的代码行以允许更改变量并标记不允许更改变量的其他代码行。 C 语言规范没有该功能。
- 循环体内的 x.inc() 呢?
- 这看起来不错。仍然有人可以在循环体内调用 x.inc() ,但大多数调用将是故意的 troll 目的,它似乎足以为错误修改提供障碍。
- @frozenca您低估了copypasta的力量。
- 太多了。我的意思是 x 仍然可以通过 x.inc() 进行修改。一个错误的复制粘贴就足以把事情搞砸。
在 C 中,你可以隐藏名称并重新声明另一个与 const 同名的标识符,但是你需要使用一些中间对象来帮助,这并不漂亮:
1
2 3 4 5 6 7 |
for (int i = 0; i < 10; ++i)
{ const int t = i, i = t; printf(“i = %d.\ “, i); // Works. i = 4; // Yields compiler error. } |
我不建议这样做,但您可以通过以下方式使其不那么难看:
1
2 |
#define Protect(Type, Original) \\
const Type Auxiliary_##Original = Original, Original = Auxiliary_##Original |
然后使用:
1
2 3 4 5 6 7 |
for (int i = 0; i < 10; ++i)
{ Protect(int, i); printf(“i = %d.\ “, i); // Works. i = 4; // Yields compiler error. } |
- 你可以在 C 中做同样的事情,但是这种方法也会触发令人讨厌的阴影警告。
- 对不起,我可能误解了,但它是如何回答这个问题的?
- @GuillaumePetitjean:在定义const int i = t;之后,无法修改for语句中定义的i,因为它是不可见的,从而满足了题主”禁止”在循环体内修改循环变量的要求.此外,循环变量 i 仍然可以使用而无需修改,因为内部 i 中提供了一个副本,并且尝试修改它(通过正常方式;可以获取其地址并做坏事)将导致编译器诊断。
- 这并不能真正解决保护循环变量在循环中不被更改的问题。范围内的新变量 i 不是循环变量,您只是引入了更多复杂性来尝试解决 C 和 C 的功能。我们都知道,增加复杂性和解决问题很少会产生好的结果,并且是最常见的代码腐烂来源之一。
- @RichardChambers:它如何不保护循环变量在循环中不被更改?在循环中,将此答案中显示的设置代码算作其中的一部分,因此”内部”是从第二个 i 的声明到右大括号的部分,不可能更改循环变量。因此它受到保护。
- 循环变量与作用域中的变量不同。如果您犯了错误并在范围之外引用了循环变量,那么它将被修改。在您的小循环的简单示例中,此类错误很容易识别。在具有多个 ifs 范围的大型复杂循环中,经过多年的维护,它是缺陷的来源。这是一种不常见且笨拙的解决方法,肯定会让下一个前来维护的程序员感到困惑。 C只是没有这个设施。
- @RichardChambers:超出什么范围?这个想法是代码 for (stuff) { const int t = i; { const int i = t; … } } 将形成循环。您不能在该范围之外引用 i,因为它不可见。您不能在 … 中引用 for 的 i,因为它不可见。如果您的意思是有人可能将代码放在 } } 内或 const int i = t; 之前,那么任何事情都是如此:如果 C 有一个名为 for 或 ProtectedFor 的构造可以满足 OP 的要求,那么有人可能会错误地使用 for 而不是 ProtectedFor…
- @RichardChambers:……没有绝对的保护可以防止编写不正确的源代码。在这种情况下,可以将它们捆绑到一个还不错的宏中。实际上,不需要额外的大括号; for (stuff); { const int t = i; const int i = t; 就足够了。显示的代码(我不推荐;答案是提供事实和教育,而不是推荐实践)实际上确实保护了循环变量不被修改。
- @EricPostpischil,我明白你的意思。尽管它非常聪明,但我不认为(正如您自己指出的那样)它是可以在实际程序中使用的解决方案(但仍然很有趣)。
编辑:回答您的最新评论:
Yes, what I’m looking for was just if there is a compiler option that warns it. If there isn’t, maybe I should just code more carefully.
不,不幸的是,在 C 中没有。是的,您应该更仔细地编码。一般来说,我建议您考虑一下为什么要拥有这样的功能。如果循环内的”保护”索引变量是一个问题,我会首先问自己我的编码风格是否有意义并且是一致的。
正如 Eric Postpischil 所注意到的,您可以使用一个临时变量将您的索引变量隐藏在一个内部块中作为 const。这样,如果您尝试修改它,编译器就会出错。
然而,这会产生阴影警告(特别是 -Wshadow 标志,这很常见):
1
2 3 4 5 |
shadow.c:9:14: warning: declaration of ‘i’ shadows a previous local [–Wshadow]
const int i = t; ^ shadow.c:4:11: note: shadowed declaration is here for (int i = 0; i < 10; ++i) |
为避免这种情况,您可以使用简单的诊断 #pragma 指令并暂时禁用该特定代码块的警告:
1
2 3 4 5 6 7 8 9 10 11 12 13 |
for (int i = 0; i < 10; ++i)
{ const int t = i; #pragma GCC diagnostic push #pragma GCC diagnostic ignored”-Wshadow” { const int i = t; printf(“i = %d\ “, i); i = 4; // Will yield a compiler error. } #pragma GCC diagnostic pop } |
上面的代码有效(当然删除了 i = 4)并且在 GCC 和 Clang 中使用 -Wall -Werror -Wshadow -pedantic 编译时没有警告。
注意:这肯定不是好的做法,但 AFAICT 它是在 C 中实现这种行为的唯一方法。
来源:https://www.codenong.com/58713726/