优化掉“while(1);”在 C++0x 中

发布于 2024-09-16 06:20:24 字数 1736 浏览 6 评论 0原文

已更新,见下文!

我听说并读到 C++0x 允许编译器为以下代码片段打印“Hello”,

#include <iostream>

int main() {
  while(1) 
    ;
  std::cout << "Hello" << std::endl;
}

这显然与线程和优化功能有关。在我看来,这可能会让很多人感到惊讶。

有人对为什么有必要允许这样做有一个很好的解释吗?作为参考,最新的 C++0x 草案位于 6.5/5

在 for 语句的情况下,在 for-init-statement 之外的循环,

  • 不调用库 I/O 函数,并且
  • 不访问或修改易失性对象,并且
  • 不执行同步操作 (1.10) 或原子操作(第 29 条)

可以假定实现终止。 [注意:这是为了允许编译器转换 即使无法证明终止,也可以删除空循环。 ——尾注]

编辑:

这篇富有洞察力的文章介绍了该标准文本

不幸的是,没有使用“未定义行为”这个词。然而,只要标准说“编译器可以假设 P”,就意味着具有 not-P 属性的程序具有未定义的语义。

这是正确的吗?编译器是否允许为上述程序打印“Bye”?


此处有一个更具洞察力的帖子,这是关于 C 的类似更改,由完成上述链接文章的 Guy 开始。除其他有用的事实外,他们提出了一个似乎也适用于 C++0x 的解决方案(更新:这不再适用于 n3225 - 见下文!)

endless:
  goto endless;

不允许编译器优化该解决方案看起来,因为这不是循环,而是跳转。另一个人总结了 C++0x 和 C201X 中提议的更改

通过编写循环,程序员断言或者 循环执行一些具有可见行为的操作(执行 I/O、访问 易失性对象,或执行同步或原子操作), 或者它最终终止。如果我违反了这个假设 通过编写一个没有副作用的无限循环,我对 编译器,并且我的程序的行为未定义。 (如果我幸运的话, 编译器可能会警告我。)该语言不提供 (不再提供?)一种表达无限循环的方法,无需 可见的行为。


2011 年 1 月 3 日更新 n3225:委员会将文本移至 1.10/24 并说

该实现可能假设任何线程最终都会执行以下操作之一:

  • 终止,
  • 调用库 I/O 函数,
  • 访问或修改易失性对象,或者
  • 执行同步操作或原子操作。

goto 技巧将不再起作用了!

Updated, see below!

I have heard and read that C++0x allows an compiler to print "Hello" for the following snippet

#include <iostream>

int main() {
  while(1) 
    ;
  std::cout << "Hello" << std::endl;
}

It apparently has something to do with threads and optimization capabilities. It looks to me that this can surprise many people though.

Does someone have a good explanation of why this was necessary to allow? For reference, the most recent C++0x draft says at 6.5/5

A loop that, outside of the for-init-statement in the case of a for statement,

  • makes no calls to library I/O functions, and
  • does not access or modify volatile objects, and
  • performs no synchronization operations (1.10) or atomic operations (Clause 29)

may be assumed by the implementation to terminate. [ Note: This is intended to allow compiler transfor-
mations, such as removal of empty loops, even when termination cannot be proven. — end note ]

Edit:

This insightful article says about that Standards text

Unfortunately, the words "undefined behavior" are not used. However, anytime the standard says "the compiler may assume P," it is implied that a program which has the property not-P has undefined semantics.

Is that correct, and is the compiler allowed to print "Bye" for the above program?


There is an even more insightful thread here, which is about an analogous change to C, started off by the Guy done the above linked article. Among other useful facts, they present a solution that seems to also apply to C++0x (Update: This won't work anymore with n3225 - see below!)

endless:
  goto endless;

A compiler is not allowed to optimize that away, it seems, because it's not a loop, but a jump. Another guy summarizes the proposed change in C++0x and C201X

By writing a loop, the programmer is asserting either that the
loop does something with visible behavior (performs I/O, accesses
volatile objects, or performs synchronization or atomic operations),
or that it eventually terminates. If I violate that assumption
by writing an infinite loop with no side effects, I am lying to the
compiler, and my program's behavior is undefined. (If I'm lucky,
the compiler might warn me about it.) The language doesn't provide
(no longer provides?) a way to express an infinite loop without
visible behavior.


Update on 3.1.2011 with n3225: Committee moved the text to 1.10/24 and say

The implementation may assume that any thread will eventually do one of the following:

  • terminate,
  • make a call to a library I/O function,
  • access or modify a volatile object, or
  • perform a synchronization operation or an atomic operation.

The goto trick will not work anymore!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

葬﹪忆之殇 2024-09-23 06:20:24

对我来说,相关的理由是:

这是为了允许编译器进行转换,例如删除空循环,即使无法证明终止也是如此。

据推测,这是因为机械地证明终止是困难,并且无法证明终止会妨碍编译器,否则编译器可以进行有用的转换,例如将非依赖操作从循环之前移动到循环之后,反之亦然,执行后-循环在一个线程中进行操作,而循环在另一个线程中执行,依此类推。如果没有这些转换,循环可能会在等待一个线程完成所述循环时阻塞所有其他线程。 (我宽松地使用“线程”来表示任何形式的并行处理,包括单独的 VLIW 指令流。)

编辑:愚蠢的示例:

while (complicated_condition()) {
    x = complicated_but_externally_invisible_operation(x);
}
complex_io_operation();
cout << "Results:" << endl;
cout << x << endl;

在这里,一个线程执行复杂的 io_操作,而另一个线程执行复杂的 io 操作会更快正在循环中进行所有复杂的计算。但是如果没有您引用的子句,编译器必须在进行优化之前证明两件事:1)complex_io_operation()不依赖于循环的结果,2) < em>循环将终止。证明 1) 非常容易,证明 2) 是停机问题。使用该子句,它可以假设循环终止并获得并行化胜利。

我还认为设计者认为生产代码中出现无限循环的情况非常罕见,通常是像事件驱动循环这样以某种方式访问​​ I/O 的情况。因此,他们悲观了罕见的情况(无限循环),转而支持优化更常见的情况(非无限,但难以机械地证明非无限循环)。

然而,这确实意味着学习示例中使用的无限循环将因此受到影响,并且会在初学者代码中引发陷阱。我不能说这完全是一件好事。

编辑:关于您现在链接的富有洞察力的文章,我想说“编译器可能假设 X 关于程序”在逻辑上相当于“如果程序不满足 X,则行为未定义”。我们可以这样表示:假设存在一个不满足属性X的程序。该程序的行为在哪里定义?标准仅定义假设属性 X 为 true 的行为。尽管标准没有明确声明该行为未定义,但它已通过省略声明了该行为未定义。

考虑一个类似的论点:“编译器可能假设变量 x 在序列点之间最多只分配一次”相当于“在序列点之间多次分配给 x 是未定义的”。

To me, the relevant justification is:

This is intended to allow compiler transfor- mations, such as removal of empty loops, even when termination cannot be proven.

Presumably, this is because proving termination mechanically is difficult, and the inability to prove termination hampers compilers which could otherwise make useful transformations, such as moving nondependent operations from before the loop to after or vice versa, performing post-loop operations in one thread while the loop executes in another, and so on. Without these transformations, a loop might block all other threads while they wait for the one thread to finish said loop. (I use "thread" loosely to mean any form of parallel processing, including separate VLIW instruction streams.)

EDIT: Dumb example:

while (complicated_condition()) {
    x = complicated_but_externally_invisible_operation(x);
}
complex_io_operation();
cout << "Results:" << endl;
cout << x << endl;

Here, it would be faster for one thread to do the complex_io_operation while the other is doing all the complex calculations in the loop. But without the clause you have quoted, the compiler has to prove two things before it can make the optimisation: 1) that complex_io_operation() doesn't depend on the results of the loop, and 2) that the loop will terminate. Proving 1) is pretty easy, proving 2) is the halting problem. With the clause, it may assume the loop terminates and get a parallelisation win.

I also imagine that the designers considered that the cases where infinite loops occur in production code are very rare and are usually things like event-driven loops which access I/O in some manner. As a result, they have pessimised the rare case (infinite loops) in favour of optimising the more common case (noninfinite, but difficult to mechanically prove noninfinite, loops).

It does, however, mean that infinite loops used in learning examples will suffer as a result, and will raise gotchas in beginner code. I can't say this is entirely a good thing.

EDIT: with respect to the insightful article you now link, I would say that "the compiler may assume X about the program" is logically equivalent to "if the program doesn't satisfy X, the behaviour is undefined". We can show this as follows: suppose there exists a program which does not satisfy property X. Where would the behaviour of this program be defined? The Standard only defines behaviour assuming property X is true. Although the Standard does not explicitly declare the behaviour undefined, it has declared it undefined by omission.

Consider a similar argument: "the compiler may assume a variable x is only assigned to at most once between sequence points" is equivalent to "assigning to x more than once between sequence points is undefined".

澉约 2024-09-23 06:20:24

有人可以很好地解释为什么有必要允许这样做吗?

是的,Hans Boehm 在 N1528 中对此提供了理由:为什么无限循环的行为未定义?,虽然这是 WG14 文档,但基本原理也适用于 C++,并且该文档同时引用了 WG14 和 WG21:

正如 N1509 正确指出的那样,当前草案基本上给出了
6.8.5p6 中无限循环的未定义行为。一个主要问题是
这样做的目的是它允许代码跨潜在的
非终止循环。例如,假设我们有以下循环,
其中 count 和 count2 是全局变量(或者有它们的地址
已取),p是局部变量,其地址尚未被取:

for (p = q; p != 0; p = p -> next) {
    ++计数;
}
for (p = q; p != 0; p = p -> 下一个) {
    ++计数2;
}

这两个循环可以合并并用下面的循环代替吗?

for (p = q; p != 0; p = p -> next) {
        ++计数;
        ++计数2;
}

如果没有 6.8.5p6 中针对无限循环的特殊分配,这
将被禁止:如果第一个循环没有终止,因为 q
指向一个循环列表,原始列表永远不会写入count2。因此
它可以与访问或的另一个线程并行运行
更新计数2。对于转换后的版本来说,这不再安全
尽管存在无限循环,但它确实访问了 count2 。因此
转型可能会引发数据竞争。

在这种情况下,编译器不太可能能够
证明循环终止;它必须明白 q 点
到一个非循环列表,我相信这超出了大多数人的能力
主流编译器,如果没有整个程序通常是不可能的
信息。

非终止循环施加的限制是对
优化编译器无法终止的循环
证明终止,以及实际的优化
非终止循环。前者比后者更常见
后者,通常优化起来更有趣。

显然也存在带有整数循环变量的 for 循环
编译器很难证明终止,并且
因此编译器很难重组循环
没有 6.8.5p6。甚至类似

for (i = 1; i != 15; i += 2)

for (i = 1; i <= 10; i += j)

处理起来似乎并不简单。 (在前一种情况下,一些基本数字
需要理论来证明终止,在后一种情况下,我们需要
了解 j 的可能值。环绕式
对于无符号整数可能会使某些推理进一步复杂化。)

这个问题似乎适用于几乎所有循环重组
转换,包括编译器并行化和
缓存优化转换,两者都有可能获得
的重要性,并且对于数字代码来说通常已经很重要。
这似乎可能会变成一笔巨大的成本
能够以最自然的方式编写无限循环,
特别是因为我们大多数人很少故意编写无限循环。

与 C 的一个主要区别是 C11 为控制常量表达式的表达式提供了例外,这与 C++ 不同,并且使您的C11 中明确定义了具体示例。

Trivial 无限循环不再是 C++ 中的 UB

随着 2024 年 3 月东京 ISO C++ 委员会会议通过 P2809, 。简单的无限循环不再是 C++ 中未定义的行为。

这并没有改变答案的本质,但确实使 C++ 与 C 更加一致。它允许用户使用大多数人认为惯用的无限循环安全性。

Does someone have a good explanation of why this was necessary to allow?

Yes, Hans Boehm provides a rationale for this in N1528: Why undefined behavior for infinite loops?, although this is WG14 document the rationale applies to C++ as well and the document refers to both WG14 and WG21:

As N1509 correctly points out, the current draft essentially gives
undefined behavior to infinite loops in 6.8.5p6. A major issue for
doing so is that it allows code to move across a potentially
non-terminating loop. For example, assume we have the following loops,
where count and count2 are global variables (or have had their address
taken), and p is a local variable, whose address has not been taken:

for (p = q; p != 0; p = p -> next) {
    ++count;
}
for (p = q; p != 0; p = p -> next) {
    ++count2;
}

Could these two loops be merged and replaced by the following loop?

for (p = q; p != 0; p = p -> next) {
        ++count;
        ++count2;
}

Without the special dispensation in 6.8.5p6 for infinite loops, this
would be disallowed: If the first loop doesn't terminate because q
points to a circular list, the original never writes to count2. Thus
it could be run in parallel with another thread that accesses or
updates count2. This is no longer safe with the transformed version
which does access count2 in spite of the infinite loop. Thus the
transformation potentially introduces a data race.

In cases like this, it is very unlikely that a compiler would be able
to prove loop termination; it would have to understand that q points
to an acyclic list, which I believe is beyond the ability of most
mainstream compilers, and often impossible without whole program
information.

The restrictions imposed by non-terminating loops are a restriction on
the optimization of terminating loops for which the compiler cannot
prove termination, as well as on the optimization of actually
non-terminating loops. The former are much more common than the
latter, and often more interesting to optimize.

There are clearly also for-loops with an integer loop variable in
which it would be difficult for a compiler to prove termination, and
it would thus be difficult for the compiler to restructure loops
without 6.8.5p6. Even something like

for (i = 1; i != 15; i += 2)

or

for (i = 1; i <= 10; i += j)

seems nontrivial to handle. (In the former case, some basic number
theory is required to prove termination, in the latter case, we need
to know something about the possible values of j to do so. Wrap-around
for unsigned integers may complicate some of this reasoning further.)

This issue seems to apply to almost all loop restructuring
transformations, including compiler parallelization and
cache-optimization transformations, both of which are likely to gain
in importance, and are already often important for numerical code.
This appears likely to turn into a substantial cost for the benefit of
being able to write infinite loops in the most natural way possible,
especially since most of us rarely write intentionally infinite loops.

The one major difference with C is that C11 provides an exception for controlling expressions that are constant expressions which differs from C++ and makes your specific example well-defined in C11.

Trvial Infinite Loops are no longer UB in C++

With the adoption of P2809 at the 2024-03 Tokyo ISO C++ Committee meeting. Trivial infinite loops are no longer undefined behavior in C++.

This does not change the essence of the answer but does move C++ to be more in line with C here. It allows users to use what most would consider to be idiomatic infinite loops safety.

£冰雨忧蓝° 2024-09-23 06:20:24

我认为正确的解释是您编辑的解释:空的无限循环是未定义的行为。

我不会说这是特别直观的行为,但这种解释比另一种解释更有意义,即编译器可以任意允许忽略无限循环而不调用 UB。

如果无限循环是 UB,则仅意味着非终止程序不被认为是有意义的:根据 C++0x,它们没有没有语义。

这确实也有一定的道理。它们是一种特殊情况,其中许多副作用不再发生(例如,main 不返回任何内容),并且许多编译器优化因必须保留无限循环而受到阻碍。例如,如果循环没有副作用,则跨循环移动计算是完全有效的,因为最终,计算将在任何情况下执行。
但是,如果循环永远不会终止,我们就无法安全地重新排列循环中的代码,因为我们可能只是更改在程序挂起之前实际执行的操作。除非我们将挂起的程序视为 UB,否则就是这样。

I think the correct interpretation is the one from your edit: empty infinite loops are undefined behavior.

I wouldn't say it's particularly intuitive behavior, but this interpretation makes more sense than the alternative one, that the compiler is arbitrarily allowed to ignore infinite loops without invoking UB.

If infinite loops are UB, it just means that non-terminating programs aren't considered meaningful: according to C++0x, they have no semantics.

That does make a certain amount of sense too. They are a special case, where a number of side effects just no longer occur (for example, nothing is ever returned from main), and a number of compiler optimizations are hampered by having to preserve infinite loops. For example, moving computations across the loop is perfectly valid if the loop has no side effects, because eventually, the computation will be performed in any case.
But if the loop never terminates, we can't safely rearrange code across it, because we might just be changing which operations actually get executed before the program hangs. Unless we treat a hanging program as UB, that is.

雨巷深深 2024-09-23 06:20:24

相关问题是编译器可以对副作用不冲突的代码进行重新排序。即使编译器为无限循环生成非终止机器代码,也可能会出现令人惊讶的执行顺序。

我相信这是正确的做法。语言规范定义了强制执行顺序的方法。如果您想要一个无法重新排序的无限循环,请编写以下内容:

volatile int dummy_side_effect;

while (1) {
    dummy_side_effect = 0;
}

printf("Never prints.\n");

The relevant issue is that the compiler is allowed to reorder code whose side effects do not conflict. The surprising order of execution could occur even if the compiler produced non-terminating machine code for the infinite loop.

I believe this is the right approach. The language spec defines ways to enforce order of execution. If you want an infinite loop that cannot be reordered around, write this:

volatile int dummy_side_effect;

while (1) {
    dummy_side_effect = 0;
}

printf("Never prints.\n");
独守阴晴ぅ圆缺 2024-09-23 06:20:24

我认为这符合这种类型的 问题,引用了另一个线程 。优化有时可以删除空循环。

I think this is along the lines of the this type of question, which references another thread. Optimization can occasionally remove empty loops.

铜锣湾横着走 2024-09-23 06:20:24

我认为这个问题也许可以最好地表述为“如果后面的代码不依赖于前面的代码,并且前面的代码对系统的任何其他部分没有副作用,则编译器的输出可以在执行前一段代码之前、之后或与前一段代码混合执行后一段代码,即使前一段代码包含循环,而不考虑前一段代码何时或是否实际完成。 ,编译器可以重写:

void testfermat(int n)
{
  int a=1,b=1,c=1;
  while(pow(a,n)+pow(b,n) != pow(c,n))
  {
    if (b > a) a++; else if (c > b) {a=1; b++}; else {a=1; b=1; c++};
  }
  printf("The result is ");
  printf("%d/%d/%d", a,b,c);
}

as

void testfermat(int n)
{
  if (fork_is_first_thread())
  {
    int a=1,b=1,c=1;
    while(pow(a,n)+pow(b,n) != pow(c,n))
    {
      if (b > a) a++; else if (c > b) {a=1; b++}; else {a=1; b=1; c++};
    }
    signal_other_thread_and_die();
  }
  else // Second thread
  {
    printf("The result is ");
    wait_for_other_thread();
  }
  printf("%d/%d/%d", a,b,c);
}

一般不是不合理的,尽管我可能担心:

  int total=0;
  for (i=0; num_reps > i; i++)
  {
    update_progress_bar(i);
    total+=do_something_slow_with_no_side_effects(i);
  }
  show_result(total);

会变成

  int total=0;
  if (fork_is_first_thread())
  {
    for (i=0; num_reps > i; i++)
      total+=do_something_slow_with_no_side_effects(i);
    signal_other_thread_and_die();
  }
  else
  {
    for (i=0; num_reps > i; i++)
      update_progress_bar(i);
    wait_for_other_thread();
  }
  show_result(total);

通过让一个CPU处理计算,另一个处理进度条更新,重写会提高效率,不幸的是,它会使进度条更新。没有应有的用处。

I think the issue could perhaps best be stated, as "If a later piece of code does not depend on an earlier piece of code, and the earlier piece of code has no side-effects on any other part of the system, the compiler's output may execute the later piece of code before, after, or intermixed with, the execution of the former, even if the former contains loops, without regard for when or whether the former code would actually complete. For example, the compiler could rewrite:

void testfermat(int n)
{
  int a=1,b=1,c=1;
  while(pow(a,n)+pow(b,n) != pow(c,n))
  {
    if (b > a) a++; else if (c > b) {a=1; b++}; else {a=1; b=1; c++};
  }
  printf("The result is ");
  printf("%d/%d/%d", a,b,c);
}

as

void testfermat(int n)
{
  if (fork_is_first_thread())
  {
    int a=1,b=1,c=1;
    while(pow(a,n)+pow(b,n) != pow(c,n))
    {
      if (b > a) a++; else if (c > b) {a=1; b++}; else {a=1; b=1; c++};
    }
    signal_other_thread_and_die();
  }
  else // Second thread
  {
    printf("The result is ");
    wait_for_other_thread();
  }
  printf("%d/%d/%d", a,b,c);
}

Generally not unreasonable, though I might worry that:

  int total=0;
  for (i=0; num_reps > i; i++)
  {
    update_progress_bar(i);
    total+=do_something_slow_with_no_side_effects(i);
  }
  show_result(total);

would become

  int total=0;
  if (fork_is_first_thread())
  {
    for (i=0; num_reps > i; i++)
      total+=do_something_slow_with_no_side_effects(i);
    signal_other_thread_and_die();
  }
  else
  {
    for (i=0; num_reps > i; i++)
      update_progress_bar(i);
    wait_for_other_thread();
  }
  show_result(total);

By having one CPU handle the calculations and another handle the progress bar updates, the rewrite would improve efficiency. Unfortunately, it would make the progress bar updates rather less useful than they should be.

忆沫 2024-09-23 06:20:24

有人可以很好地解释为什么有必要允许这样做吗?

我有一个解释为什么有必要允许,假设 C++ 仍然有志成为一种适合性能关键的低级编程的通用语言。

这曾经是一个工作的、严格遵守的独立 C++ 程序:

int main()
{
  setup_interrupts();
  for(;;)
  {}
}

上面是在许多低端微控制器系统中编写 main() 的完美且常见的方法。整个程序执行在中断服务例程内完成(或者对于 RTOS,它可以是进程)。而且 main() 可能绝对不允许返回,因为这些是裸机系统,没有人可以返回。

可使用上述设计的典型现实示例包括 PWM/电机驱动器微控制器、照明控制应用、简单的调节器系统、传感器应用、简单的家用电子产品等。

不幸的是,C++ 的变化使得该语言无法用于此类嵌入式系统编程。如果移植到较新的 C++ 编译器,现有的实际应用程序(如上述应用程序)将会以危险的方式崩溃。

C++20 6.9.2.3 前进进度 [intro.progress]

该实现可能假设任何线程最终都会执行以下操作之一:
(1.1) — 终止,
(1.2) — 调用库 I/O 函数,
(1.3) — 通过易失性泛左值执行访问,或
(1.4) — 执行同步操作或原子操作。

让我们逐一讨论上述之前严格遵守的独立 C++ 程序:

  • 1.1。正如任何嵌入式系统初学者都可以告诉委员会的那样,裸机/RTOS 微控制器系统永远不会终止。因此,循环不能终止,否则 main() 将变成失控代码,这将是严重且危险的错误情况。
  • 1.2 不适用。
  • 1.3 不一定。有人可能会争辩说,for(;;) 循环是喂养看门狗的正确位置,而这又是看门狗写入易失性寄存器时的副作用。但可能存在一些实现原因导致您不想使用看门狗。无论如何,规定应该使用看门狗并不是 C++ 的事。
  • 1.4 不适用。

因此,C++ 比以前更不适合嵌入式系统应用程序。


这里的另一个问题是该标准谈到“线程”,这是更高层次的概念。在现实世界的计算机上,线程是通过称为中断的低级概念来实现的。中断在竞争条件和并发执行方面与线程类似,但它们不是线程。在低级系统上,只有一个核心,因此通常一次只执行一个中断(有点像过去在单核 PC 上工作的线程)。

如果不能有中断,就不能有线程。因此线程必须由比 C++ 更适合嵌入式系统的低级语言来实现。选项是汇编程序或 C。


通过编写循环,程序员可以断言循环执行一些具有可见行为的操作(执行 I/O、访问易失性对象或执行同步或原子操作),或者断言它最终终止。

这是完全误导的,并且是由从未使用过微控制器编程的人清楚地写的。


那么,那些“因为某些原因”拒绝将代码移植到 C 的少数剩余 C++ 嵌入式程序员应该做什么呢?您必须在 for(;;) 循环中引入副作用:

int main()
{
  setup_interrupts();
  for(volatile int i=0; i==0;)
  {}
}

或者,如果您按照应有的方式启用了看门狗,请将其输入到 for(;;) 中> 循环。

Does someone have a good explanation of why this was necessary to allow?

I have an explanation of why it is necessary not to allow, assuming that C++ still has the ambition to be a general-purpose language suitable for performance-critical, low-level programming.

This used to be a working, strictly conforming freestanding C++ program:

int main()
{
  setup_interrupts();
  for(;;)
  {}
}

The above is a perfectly fine and common way to write main() in many low-end microcontroller systems. The whole program execution is done inside interrupt service routines (or in case of RTOS, it could be processes). And main() may absolutely not be allowed to return since these are bare metal systems and there is nobody to return to.

Typical real-world examples of where the above design can be used are PWM/motor driver microcontrollers, lighting control applications, simple regulator systems, sensor applications, simple household electronics and so on.

Changes to C++ have unfortunately made the language impossible to use for this kind of embedded systems programming. Existing real-world applications like the ones above will break in dangerous ways if ported to newer C++ compilers.

C++20 6.9.2.3 Forward progress [intro.progress]

The implementation may assume that any thread will eventually do one of the following:
(1.1) — terminate,
(1.2) — make a call to a library I/O function,
(1.3) — perform an access through a volatile glvalue, or
(1.4) — perform a synchronization operation or an atomic operation.

Lets address each bullet for the above, previously strictly conforming freestanding C++ program:

  • 1.1. As any embedded systems beginner can tell the committee, bare metal/RTOS microcontroller systems never terminate. Therefore the loop cannot terminate either or main() would turn into runaway code, which would be a severe and dangerous error condition.
  • 1.2 N/A.
  • 1.3 Not necessarily. One may argue that the for(;;) loop is the proper place to feed the watchdog, which in turn is a side effect as it writes to a volatile register. But there may be implementation reasons why you don't want to use a watchdog. At any rate, it is not C++'s business to dictate that a watchdog should be used.
  • 1.4 N/A.

Because of this, C++ is made even more unsuitable for embedded systems applications than it already was before.


Another problem here is that the standard speaks of "threads", which are higher level concepts. On real-world computers, threads are implemented through a low-level concept known as interrupts. Interrupts are similar to threads in terms of race conditions and concurrent execution, but they are not threads. On low-level systems there is just one core and therefore only one interrupt at a time is typically executed (kind of like threads used to work on single core PC back in the days).

And you can't have threads if you can't have interrupts. So threads would have to be implemented by a lower-level language more suitable for embedded systems than C++. The options being assembler or C.


By writing a loop, the programmer is asserting either that the loop does something with visible behavior (performs I/O, accesses volatile objects, or performs synchronization or atomic operations), or that it eventually terminates.

This is completely misguided and clearly written by someone who has never worked with microcontroller programming.


So what should those few remaining C++ embedded programmers who refuse to port their code to C "because of reasons" do? You have to introduce a side effect inside the for(;;) loop:

int main()
{
  setup_interrupts();
  for(volatile int i=0; i==0;)
  {}
}

Or if you have the watchdog enabled as you ought to, feed it inside the for(;;) loop.

許願樹丅啲祈禱 2024-09-23 06:20:24

如果它根本是一个无限循环,那么对于非平凡情况,编译器是不可判定的。

在不同的情况下,您的优化器可能会为您的代码达到更好的复杂度级别(例如,它是 O(n^2),优化后您将获得 O(n) 或 O(1))。

因此,在 C++ 标准中包含这样一条不允许删除无限循环的规则将使许多优化变得不可能。大多数人不希望这样。我认为这完全回答了你的问题。


另一件事:我从未见过任何有效的示例,您需要一个不执行任何操作的无限循环。

我听说过的一个例子是一个丑陋的黑客行为,确实应该解决:它是关于嵌入式系统的,其中触发重置的唯一方法是冻结设备,以便看门狗自动重新启动它。

如果您知道任何有效/好的示例,您需要一个不执行任何操作的无限循环,请告诉我。

It is not decidable for the compiler for non-trivial cases if it is an infinite loop at all.

In different cases, it can happen that your optimiser will reach a better complexity class for your code (e.g. it was O(n^2) and you get O(n) or O(1) after optimisation).

So, to include such a rule that disallows removing an infinite loop into the C++ standard would make many optimisations impossible. And most people don't want this. I think this quite answers your question.


Another thing: I never have seen any valid example where you need an infinite loop which does nothing.

The one example I have heard about was an ugly hack that really should be solved otherwise: It was about embedded systems where the only way to trigger a reset was to freeze the device so that the watchdog restarts it automatically.

If you know any valid/good example where you need an infinite loop which does nothing, please tell me.

骄兵必败 2024-09-23 06:20:24

我认为值得指出的是,除了通过非易失性、非同步变量与其他线程交互这一事实之外,循环将是无限的,现在可以通过新编译器产生不正确的行为。

换句话说,使您的全局变量以及通过指针/引用传递到此类循环中的参数变得易失性。

I think it's worth pointing out that loops which would be infinite except for the fact that they interact with other threads via non-volatile, non-synchronised variables can now yield incorrect behaviour with a new compiler.

I other words, make your globals volatile -- as well as arguments passed into such a loop via pointer/reference.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文