不需要循环变量的C++11基于范围的for循环

65

在C++中,我需要迭代一定次数,但不需要迭代变量。例如:

for( int x=0; x<10; ++x ) {
    /* code goes here, i do not reference "x" in this code */
}

我意译为:“我意识到我可以通过使用lambda表达式或命名函数来做到这一点,但是这个问题特别关注for循环。”

“我曾希望C++11的范围for循环能够有所帮助:”

for( auto x : boost::irange(0,10) ) {
    /* code goes here, i do not reference "x" in this code */
}

但是以上代码会出现“未引用的本地变量”警告,因为我从未明确引用过x。

我想知道是否有更优雅的方式编写上面的循环,以便代码不会生成“未引用的局部变量”警告。


2
为了可能澄清这一点,我的理解是OP试图使用基于范围的for循环来实现这一点。然而,类似for (auto x : boost::irange(0, 10)) f();的代码会给出一个警告,因为x没有被使用。 - chris
9
但是变量确实被引用了(在循环条件中)。编译器绝不能为该代码发出警告。 - rodrigo
4
@rodrigo:我不这么认为,循环条件使用了一个隐藏的迭代器。x被写入但从未被读取,导致编译器发出警告。 - Mooing Duck
2
@MooingDuck x<10; 读取 x 吗? - Yakk - Adam Nevraumont
1
@Yakk: for (auto x : boost::irange(0, 10)) 不包含表达式 x<10。它更像是一个语句 auto x = *__secret_iterator1; - aschepler
显示剩余9条评论
11个回答

47

现在编辑时,不再声明任何循环变量。

template <typename F>
void repeat(unsigned n, F f) {
    while (n--) f();
}

用法如下:

repeat(10, f);

或者

repeat(10, [] { f(); });

或者

int g(int);
repeat(10, std::bind(g, 42));

您可以在http://ideone.com/4k83TJ上实时查看。


该链接可供实时查看。

@chris 这取决于你想让代码编译吗?还是想让它做些什么?在 lambda 实例上操作可以改善这两个问题。 :) - Yakk - Adam Nevraumont
@chris 我在函数模板中使用了F()而不是f()。现已修复。 - sehe
@sehe,你也可以在[]{ f(); }中省略(),并在[]中加入&,这样它就像一个子范围一样运行。 - Yakk - Adam Nevraumont
@Yakk 我会把这个交给实际用户。我想展示的只是你可以传递一个lambda实例、绑定表达式,或者任何可调用的东西。 - sehe
@sehe,哦,这样就有意义多了 :) - chris
显示剩余4条评论

23

可能有一种方式可以实现,但我非常怀疑它是否更加优雅。您在第一个循环中所拥有的已经是正确的方式,通过限制循环变量的作用域和生命周期来实现。

我会简单地忽略未使用变量的警告(毕竟这只是编译器提示某些东西可能有问题的指示),或者使用编译器提供的工具(如果可用)在该点上关闭警告。

这可能在某些环境下可以通过某种特殊的#pragma实现,或者某些实现允许您执行以下操作:

for (int x = 0; x < 10; ++x) {
    (void)x;

    // Other code goes here, that does not reference "x".
}

我曾见过在函数体中用 void 技巧处理未使用的参数。


1
到目前为止,这是唯一值得点赞的答案。可能值得一提的是,C++11规范指出循环变量(即“for-range-declaration”)是必需的。 - Nemo
我认为编译器为此发出的警告是完全错误的。 - user319799
15
我认为,实际上建议人们忽略编译器警告是危险的。这会导致程序编译出许多(被忽略的)警告,从而掩盖了偶尔想要、重要和有用的警告。正因为这个原因,我永远不会接受一个补丁,除非它在没有任何警告的情况下就能编译成功... - cmaster - reinstate monica
2
@cmaster,我并不建议他们盲目忽略所有警告,只是在代码的那个点上忽略那个警告。一旦你理解了警告并意识到自己比编译器更懂,这样做就没问题了。由于可能是我使用了“任何”这个词导致你认为我是指“所有”,而不是我本意的零或一个,所以我已经将其删除 :-) - paxdiablo
5
有些警告很难忽略。这意味着只要你维护代码,就必须记住它,如果新的开发人员来了,他必须明白这个警告是无害的...我肯定会使用某种UNUSED(var)宏来抑制警告。 - Karoly Horvath
@Karoly,是的,如果你有类似#warning这样的东西可用,你可以输出一个警告来忽略这个警告 :-) 但你说得对,最好使用#pragma或某种使用变量的宏来抑制它。 - paxdiablo

9
假设10是一个编译时常量...
#include <cstddef>
#include <utility>
template<std::size_t N>
struct do_N_times_type {
  template<typename Lambda>
  void operator()( Lambda&& closure ) const {
    closure();
    do_N_times_type<N-1>()(std::forward<Lambda>(closure));
  }
};
template<>
struct do_N_times_type<1> {
  template<typename Lambda>
  void operator()( Lambda&& closure ) const {
    std::forward<Lambda>(closure)();
  }
};
template<>
struct do_N_times_type<0> {
  template<typename Lambda>
  void operator()( Lambda&& closure ) const {
  }
};

template<std::size_t N, typename Lambda>
void do_N_times( Lambda&& closure ) {
  do_N_times_type<N>()( std::forward<Lambda>(closure) );
};
#include <iostream>
void f() {
  std::cout << "did it!\n";
}
int main() {
  do_N_times<10>([&]{
    f();
  });
}

或者只是
int main() {
  do_N_times<10>(f);
}

其他荒谬的方法:

编写一个范围迭代器(我称之为index),它产生整数类型的迭代器范围(我默认使用std::size_t)。然后输入:

for( auto _:index_range(10) )

这段代码使用了一个变量 (_),但看起来非常令人困惑。

另一种疯狂的方法是创建一个类似 Python 的生成器。编写一个生成器包装器,它接收一个可迭代范围并产生一个返回范围的 value_typestd::optional 的函数并不复杂。

然后我们可以这样做:

auto _ = make_generator( index_range(10) );
while(_()) {
}

这将创建一个临时变量,并且更加晦涩。

我们可以编写一个在生成器上操作的循环函数:

template<typename Generator, typename Lambda>
void While( Generator&& g, Lambda&& l ) {
  while(true) {
    auto opt = g();
    if (!opt) return;
    l(*opt);
  }
}

然后我们可以像这样调用:

While( make_generator( index_range(10) ), [&](auto&&){
  f();
});

但是这样会在函数中创建一些临时变量,而且比上一个方法更荒谬,并依赖于尚未最终确定的C++1y的功能。

这些都是我尝试创建无需变量重复10次某些操作的方式。

但实际上,我只会使用循环。

您几乎可以通过键入x=x;来阻止警告。

或者编写一个函数。

template<typename Unused>
void unused( Unused&& ) {}

并且调用 unused(x); -- 变量 x 被使用,但其名称在内部被省略了,因此编译器可能不会在内部警告您。

因此,请尝试这样做:

template<typename Unused>
void unused( Unused&& ) {}
for(int x{};x<10;++x) {
  unused(x);
  f();
}

应该抑制警告,并且更易于理解。


2
为什么1被特殊处理?看起来没有必要。 - Mooing Duck
8
为什么要使用模板魔法?我的回答只有3行,并且对于编译时常量的 n,也可以完全展开为循环。(最近在gcc/clang上验证过) - sehe
2
仅仅通过看代码,我就能够判断出它是你写的。 - David G
5
看起来真是可怕!这一段代码量太离谱了,为了避免本来就不应该出现的编译器警告。我是说,这在一种代码压缩比赛中还挺令人印象深刻的,但是在实际生产环境中使用这样的代码简直就是不可能的。大多数人花费在理解这些代码上的时间根本不值得为了省去一个迭代变量而获得的那些抽象好处。 - Darrel Hoffman
1
@DarrelHoffman 当然可以,但你能否想出一种更简洁的方法,在不使用变量的情况下迭代一个函数10次?这是一个棘手的问题。我包含了我的失败尝试,以便有人可以想出一种“改进”它们的方法。(嗯,你也可以使用boost预处理器宏来完成...)我同意应该只是抑制警告(并提供了一种方法来做到这一点)。 - Yakk - Adam Nevraumont
显示剩余5条评论

9

实际上有一种方法可以使这个工作起来。你所需要做的就是返回一个长度由你提供的常量指定的std::array

template <int N>
using range = std::array<int, N>;

int main()
{
    for (auto x : range<5>())
    {
        std::cout << "Awesome\n";
    }
}

输出:

牛逼
牛逼
牛逼
牛逼
牛逼

这里是演示。

注意:假设范围指示器是编译时常量,如果必须使用变量,请确保其已经正确地标记为 constexpr


1
风格得分满分,但要注意,即使将“auto x”更改为“const auto& x”,Clang 3.0仍会生成“subq%rsp,$hugenumber ... callq memset”,除了循环之外。因此,在编译器支持跟上之前,使用这种方法处理“range<100000>”可能不是一个好主意。 - Quuxplusone

5
在我看来,你误用了基于范围的循环。基于范围的循环应该在逻辑上是:“对于集合中的每个元素,执行某些操作”。整个想法是摆脱索引变量,因为它并不重要。如果你有一个集合,你应该使用必要的API来启用基于范围的迭代。如果你没有一个集合,你就没有理由使用基于范围的循环(实际上,这就是编译器在不太明确的方式中所暗示的)。在这种情况下,正常的for/while循环是自然的选择。

4

没有简单遍历几个数字的方法来使用基于范围的for循环。

C++11基于范围的循环需要一个范围表达式,可以是:

  • 一个数组或
  • 一个具有以下之一的类:
    • 成员函数begin()end()
    • 可用的自由函数begin()end()(通过ADL)

除此之外:基于范围的for循环会产生一些额外开销:

for ( for_range_declaration : expression ) statement

扩展为
range_init = (expression)
{
  auto && __range = range_init;
  for ( auto __begin = begin_expr,
  __end = end_expr;
  __begin != __end;
  ++__begin ) {
    for_range_declaration = *__begin;
    statement;
  }
}

其中begin_expr和end_expr是通过数组检查或begin()/end()对获得的。

我认为使用简单的for循环没有更好的方法了。特别是在性能方面。 没有调用,只有一个普通的循环。

我唯一想到的使它更优雅的方法(其中优雅显然取决于我的观点)是在这里使用大小或无符号类型:

for(size_t x(0U); x<10U; ++x) f();

for(unsigned x{},x<10u;++x) 可以说更加优雅。 - Mooing Duck
@MooingDuck for(int x{};x<10;++x) 节省字符! - Yakk - Adam Nevraumont
@Yakk:我们应该发布一个问题,看看我们是否可以使语法更加优雅。 - Mooing Duck
@MooingDuck 我不同意。1. 因为你有一个拼写错误,所以它甚至无法编译。2. 当前的 MSVC 版本中没有支持这种大括号初始化的功能。为了两个字符而牺牲可移植性并不优雅。 - Pixelchemist
在我看来,可移植性和优雅性是正交的。有很多优雅的代码并不具备可移植性。另外,虽然代码理论上是可移植的,但它只适用于遵循这一领域标准的编译器。在现实世界中,我完全理解你的观点,并且绝不会做我刚才输入的那样的事情。 - Mooing Duck

4
您可以使用STL与lambda表达式一起使用。
#include <algorithm>
#include <iostream>

int main() {
    int a[] = {1,2,3,4,5,6};

    std::for_each(std::begin(a),
            std::end(a),
            [](int){std::cout << "Don't care" << std::endl;});
}

这种方法同样适用于任意容器,比如向量或列表。如果有 vector<int> a,那么你可以调用 a.begin()a.end()。请注意,你也可以使用函数指针来替代 lambda 表达式。

上述方法保留了您使用 foreach 的概念,同时不会抱怨未使用的参数。


好的,干净的,惯用的 - kfmfe04

4

1
我认为在那种情况下,我宁愿直接写出for循环而不使用基于范围的语法(即for(uint x=0; x<N; ++x))。这很不幸——基于范围的语法非常简洁明了,我真的希望它能更直接地支持这种用例! - nonagon

2
这适用于GCC、clang以及任何支持GNU属性的编译器:
for( [[gnu::unused]] auto x : boost::irange(0,10) ) {

该代码应在任何C++11编译器中编译,但如果编译器不识别GNU属性,则可能无法抑制警告。


1

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