在C++中是否有类似于Python的“for ... else”循环结构的等价物?

83

Python具有一个有趣的for语句,它允许您指定一个else子句。

在类似于这样的结构中:


for i in foo:
  if bar(i):
    break
else:
  baz()

else子句会在for循环执行完毕后被执行,但仅当for正常结束时(非通过break)。

我想知道C++中是否有等价的功能?我能使用for ... else吗?


4
这是一个偶尔会被提出的有趣想法。出于好奇,这样的结构在实际代码中使用频率如何? - Kerrek SB
5
这是真正的问题。当我开始学习Python时,我觉得这种构造对初学者来说有点琐碎和混乱;我找不到使用这种结构的紧迫需求。 - legends2k
2
@m.wasowski:真的吗?通常标准的早期返回对于类似查找算法来说就足够了,不需要单独的“循环完成”块。 - Kerrek SB
6
我已经被说服这种构造很有用。理由是:if (c)while (c)for (...; c; ...) 这三者都会评估条件 c,但目前只有第一个能让你得到表达式的结果。其他两个则丢弃了这个信息,这违反了不应该丢弃作为计算一部分获得的结果的原则。 - Kerrek SB
1
@agomcas:not break 语句块 的好处在于使用现有的关键字,以实现更好的向后兼容性(并且强烈表明其运行条件)。在C++中,使用else会与现有代码冲突,例如 if (...) for (...) ...; else ... - Tony Delroy
显示剩余6条评论
14个回答

42

如果不介意使用 goto,也可以按以下方式完成。这种方法可以避免额外的 if 检查和更高范围变量声明。

for(int i = 0; i < foo; i++)
     if(bar(i))
         goto m_label;
baz();

m_label:
...

3
据我所理解,问题不在于要使用什么,而是关于C++中可能的实现方式。 - ifyalciner
6
@MatthiasB 我也不认为“goto”已经过时了。因为仍然有一些领域需要在C++中编程,需要省略额外的“if”检查和声明以考虑性能和内存问题。 - ifyalciner
5
跳转语句并不那么糟糕,在这种情况下我会使用它。我认为这实际上比基于标志的解决方案更易读。 - Андрей Беньковский
4
如果我们考虑语言的等效性,那么这就是唯一有效的答案。Python没有goto语句,而C++有,所有其他例子,如使用return、临时变量和break或异常,在Python中都可以完成。此外,这种情况(for else)以及跳出嵌套循环可能是C++中仅有的两种有效应用goto语句的情况。 - 0kcats
5
goto语句被明确定义,并且通常是处理复杂循环最易于理解(即最易于维护)的方案。这个方案得到我的支持(我会给出类似的建议)。 - kamikaze
显示剩余14条评论

39

一个更简单表达实际逻辑的方法是使用std::none_of

if (std::none_of(std::begin(foo), std::end(foo), bar))
    baz();

如果C++17的范围提案被接受,希望这将简化为:
if (std::none_of(foo, bar)) baz();

11
不完全等价。例如,它不允许通过“bar”修改容器的元素。更不用说,这让我(一个C++开发人员)去查看帮助文档,而原始的Python示例我立即就能理解。STL变成了一堆泥。 - 0kcats
2
我是在参考这里的帮助页面:http://www.cplusplus.com/reference/algorithm/none_of/ 或者 http://en.cppreference.com/w/cpp/algorithm/all_any_none_of。两者都说谓词不应修改参数。 - 0kcats
@0kcats:嗯...25.1/8确实说:“函数对象pred不得通过解引用的迭代器应用任何非常量函数。”,所以你是对的。有趣!相关讨论在这里。干杯 - Tony Delroy

15

这是我的C++草稿实现:

bool other = true;
for (int i = 0; i > foo; i++) {
     if (bar[i] == 7) {
          other = false;
          break;
     }
} if(other)
     baz();

糟糕,问题已解决。 - Easton
2
+1是因为我认为这个解决方案最清楚地表达了意图。 - Thijs van Dien
8
为什么是 i > foo?不应该是 i < foo 吗? - ferdynator

15

是的,您可以通过以下方式实现相同的效果:

auto it = std::begin(foo);
for (; it != std::end(foo); ++it)
     if(bar(*it))
         break;
if(it == std::end(foo))
    baz();

2
请注意,这里从0计数到foo,而Python循环遍历可迭代对象foo的元素。虽然与end()相比可能很容易调整。 - user395760
3
不。Python循环根本不计数。i 可能从来不是整数。这更像是C++11的 for (auto i : foo) 循环。 - user395760
3
for(auto it = begin(something); it != end(something); ++i) 这行代码意为:对于定义的迭代器it,它从something的起始位置开始,直到遍历完整个something(不包括结尾位置),每一次循环增加一个元素进行迭代。 if (it == end(something)) 判断当前迭代器it是否已经到达了something的末尾。另外请记住,每当您进行不必要的后缀递增时,都会有一只小猫死去。 - m.wasowski
1
@Joeytje50 i++ 可能会慢得多,因为它涉及创建一个新对象;在像 ++i 可以使用的地方,使用它是一个坏习惯,在体面的代码中你不会找到它。就个人而言,它迫使我不必要地减慢速度并思考是否有理由使用它,或者只是草率的代码。即使只有一秒钟,这可能也很烦人。 - m.wasowski
1
@Joeytje50 请注意,这是针对不同编程语言的答案。例如,迭代器不像int那样是原始类型。此外,请查看下面的评论,您将找到所有使用++i作为默认值的原因。而且,在我看来,出于一致性考虑,原始类型(如int)也应该使用相同的方式。 - m.wasowski
显示剩余2条评论

14
你可以使用lambda函数来实现这个功能:
[&](){
  for (auto i : foo) {
    if (bar(i)) {
      // early return, to skip the "else:" section.
      return;
    }
  }
  // foo is exhausted, with no item satisfying bar(). i.e., "else:"
  baz();
}();

这应该像Python的"for..else"一样工作,并且相比其他解决方案具有一些优点:

  • 它是"for..else"的真正插入式替代: "for"部分可以有副作用(不像none_of,其谓词不能修改其参数),并且它可以访问外部范围。
  • 比定义特殊宏更易读。
  • 不需要任何特殊的标志变量。

但是...我会自己使用笨拙的标志变量。


4
我不知道在C/C++中实现这一点的优雅方法(不涉及标志变量)。建议的其他选项比这更加可怕...
为了回答@Kerrek SB关于现实生活中的用途,我在我的代码中找到了一些例子(简化片段)。
示例1:典型的查找/失败。
for item in elements:
    if condition(item):
        do_stuff(item)
        break
else: #for else
    raise Exception("No valid item in elements")

示例2:尝试次数有限

for retrynum in range(max_retries):
    try:
        attempt_operation()
    except SomeException:
        continue
    else:
        break
else: #for else
    raise Exception("Operation failed {} times".format(max_retries))

第一个示例可以使用Python的内置函数anymap或迭代器推导式替换为if语句。 - Андрей Беньковский
@АндрейБеньковский 但是我相信使用any()结构,无法恢复满足条件的元素。在for...else情况下,它存储在item中,您可以对其执行其他操作。 - agomcas
@agomcas 我继续进行我的实验。链接。我不确定哪个解决方案更易读。 - Андрей Беньковский
@agomcas 更加改进的版本:http://melpon.org/wandbox/permlink/xiQBiPYY9zBV0ceY - Андрей Беньковский
@АндрейБеньковский 对我来说,for...else的可读性要好得多。另一种选择涉及创建一个单独的哨兵对象+使用从filter返回的生成器+使用带有默认返回值的next(顺便说一下,这是我第一次遇到这种用法)。它从一个构造开始,你可以在介绍编程之后就解释清楚'if...else'和'for',变成了一个需要相当多Python知识的构造(而且我敢打赌在资源方面更昂贵),只为了匹配相同数量的行。真的不是竞争对手吗? - agomcas
显示剩余2条评论

3

类似于:

auto it = foo.begin(), end = foo.end();
while ( it != end && ! bar( *it ) ) {
    ++ it;
}
if ( it != foo.end() ) {
    baz();
}

应该就万事大吉了,而且它避免了无组织的break


1
在我看来,break 是可以的,而且无论如何,这种改变与实际内容(it != foo.end())完全不相关。 - user395760
@BenVoigt 很好的观点。这就是打字太快的结果。应该是一个while循环;我会修复它的。 - James Kanze

2

在C++中没有这样的语言结构,但是由于预处理器的“魔力”,您可以自己创建一个。例如,像这样的东西(C++11):

#include <vector>
#include <iostream>
using namespace std;

#define FOR_EACH(e, c, b) auto e = c.begin(); for (; e != c.end(); ++e) {b} if (e == c.end()) {}

int main()
{
    vector<int> v;
    v.push_back(1);
    v.push_back(2);

    FOR_EACH(x, v, {
        if (*x == 2) {
            break;
        }        
        cout << "x = " << *x << " ";
    })
    else {
        cout << "else";
    }

    return 0;
}

这应该输出 x = 1 else
如果你把 if (*x == 2) { 改为 if (*x == 3) {,输出应该是 x = 1 x = 2
如果你不喜欢在当前范围内添加变量,可以稍微修改一下:
#define FOR_EACH(e, c, b, otherwise) {auto e = c.begin(); for (; e != c.end(); ++e) {b} if (e == c.end()) {} otherwise }

然后使用将是:
FOR_EACH(x, v, {
    if (*x == 2) {
        break;
    }        
    cout << "x = " << *x << " ";
},
else {
    cout << "else";
})

当然不是完美的,但是如果小心使用,可以节省一些打字时间,而且如果经常使用,它会成为项目“词汇”的一部分。


4
牺牲易读性来省下五秒钟的打字时间通常是一个非常糟糕的主意。 - Андрей Беньковский
可读性是主观的,而打字则不是。这是一个权衡,就像其他事情一样。我并不提倡使用它,只是展示如何在C++中实现“for each...else”而不使用goto(它本身也有问题)。 - srdjan.veljkovic
3
这里使用一个goto模式比声明宏要好得多,追求可读性和可维护性。 - 0kcats
我不确定宏在我的作用域中留下一个新变量的感觉如何。在你的例子中,else语句后会留下一个迭代器x。没有什么好的方法可以避免这个问题... - nonsensickle
我添加了一个避免引入新变量到当前作用域的方法示例。我并不是说这很好,但它却起到了作用。 :) - srdjan.veljkovic

2

这不仅在C++中可以实现,而且在C语言中也可以实现。为了使代码易于理解,我将继续使用C++:

for (i=foo.first(); i != NULL || (baz(),0); i = i.next())
{
    if bar(i):
        break;
}

我怀疑我不会在代码审查中通过这个,但它能够工作且高效。在我看来,这也比其他一些建议更清晰。


0

可能没有一种解决方案适用于所有问题。在我的情况下,一个标志变量和一个基于范围的for循环与auto说明符最好地工作。这是相关代码的等效形式:

bool none = true;
for (auto i : foo) {
  if (bar(i)) {
    none = false;
    break;
  }
}
if (none) baz();

这比使用迭代器打字更少。特别是,如果您使用for循环来初始化变量,您可以使用它来代替布尔标志。

由于auto类型推断,它比std::none_of更好,如果您想要内联条件而不是调用bar()(并且如果您没有使用C++14)。

我遇到了两种情况,代码看起来像这样:

for (auto l1 : leaves) {
  for (auto x : vertices) {
    int l2 = -1, y;
    for (auto e : support_edges[x]) {
      if (e.first != l1 && e.second != l1 && e.second != x) {
        std::tie(l2, y) = e;
        break;
      }
    }
    if (l2 == -1) continue;

    // Do stuff using vertices l1, l2, x and y
  }
}

这里不需要迭代器,因为v指示了是否发生了break

使用std::none_of需要在lambda表达式的参数中显式指定support_edges[x]元素的类型。


为什么需要指定类型? - Yam Marcovic
@YamMarcovic 承认,我不知道你可以在lambda中使用auto specifier。但是它只适用于C++14,而我正在使用C++11。 - arekolek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接