让我们考虑一个用C ++ 11编写的模板函数,它迭代一个容器。 请不要考虑范围循环语法,因为我正在使用的编译器尚不支持它。
template <typename Container>
void DoSomething(const Container& i_container)
{
// Option #1
for (auto it = std::begin(i_container); it != std::end(i_container); ++it)
{
// do something with *it
}
// Option #2
std::for_each(std::begin(i_container), std::end(i_container),
[] (typename Container::const_reference element)
{
// do something with element
});
}
for循环vs std::for_each
的优缺点在于:
a)表现? (我认为没有任何区别)
b)可读性和可维护性?
在这里,我看到了for_each
的许多缺点。循环会不会接受c风格的数组。 lambda形式参数的声明是如此冗长,不可能在那里使用auto
。不可能突破for_each
。
在C ++之前的11天中,针对for
的参数需要指定迭代器的类型(不再容纳)并且很容易错误地输入循环条件(我从来没有在10年内完成了这样的错误。)
作为结论,我对for_each
的看法与普遍看法相矛盾。我在这里缺少什么?
答案 0 :(得分:32)
我认为到目前为止答案还没有涵盖其他一些差异。
for_each
可以接受任何适当的可调用对象,允许人们为不同的for循环“循环”循环体。例如(伪代码)
for( range_1 ) { lengthy_loop_body } // many lines of code
for( range_2 ) { lengthy_loop_body } // the same many lines of code again
变为
auto loop_body = some_lambda; // many lines of code here only
std::for_each( range_1 , loop_body ); // a single line of code
std::for_each( range_2 , loop_body ); // another single line of code
从而避免重复并简化代码维护。 (当然,在一个有趣的混合风格中,人们也可以使用for
循环的类似方法。)
另一个不同之处在于断开循环(在break
循环中使用return
或for
。据我所知,在for_each
循环中,这只能通过抛出异常来完成。例如
for( range )
{
some code;
if(condition_1) return x; // or break
more code;
if(condition_2) continue;
yet more code;
}
变为
try {
std::for_each( range , [] (const_reference x)
{
some code;
if(condition_1) throw x;
more code;
if(condition_2) return;
yet more code;
} );
} catch(const_reference r) { return r; }
与调用具有循环体和函数体(在循环周围)范围内的对象的析构函数具有相同的效果。
for_each
的主要好处是,恕我直言,当普通迭代不那么有效时,可以为某些容器类型重载它。例如,考虑一个容纳数据块链表的容器,每个块包含一个连续的元素数组,类似于(省略不相关的代码)
namespace my {
template<typename data_type, unsigned block_size>
struct Container
{
struct block
{
const block*NEXT;
data_type DATA[block_size];
block() : NEXT(0) {}
} *HEAD;
};
}
然后这个类型的适当的前向迭代器需要在每个增量处检查块的结尾,并且比较运算符需要比较块指针和每个块内的索引(省略不相关的代码):
namespace my {
template<typename data_type, unsigned block_size>
struct Container
{
struct iterator
{
const block*B;
unsigned I;
iterator() = default;
iterator&operator=(iterator const&) = default;
iterator(const block*b, unsigned i) : B(b), I(i) {}
iterator& operator++()
{
if(++I==block_size) { B=B->NEXT; I=0; } // one comparison and branch
return*this;
}
bool operator==(const iterator&i) const
{ return B==i.B && I==i.I; } // one or two comparisons
bool operator!=(const iterator&i) const
{ return B!=i.B || I!=i.I; } // one or two comparisons
const data_type& operator*() const
{ return B->DATA[I]; }
};
iterator begin() const
{ return iterator(HEAD,0); }
iterator end() const
{ return iterator(0,0); }
};
}
这种类型的迭代器可以与for
和for_each
一起正常工作,例如
my::Container<int,5> C;
for(auto i=C.begin();
i!=C.end(); // one or two comparisons here
++i) // one comparison here and a branch
f(*i);
但每次迭代需要两到三次比较以及分支。一种更有效的方法是重载for_each()
函数以分别循环块指针和索引:
namespace my {
template<typename data_type, int block_size, typename FuncOfDataType>
FuncOfDataType&&
for_each(typename my::Container<data_type,block_size>::iterator i,
typename my::Container<data_type,block_size>::iterator const&e,
FuncOfDataType f)
{
for(; i.B != e.B; i.B++,i.I=0)
for(; i.I != block_size; i.I++)
f(*i);
for(; i.I != e.I; i.I++)
f(*i);
return std::move(f);
}
}
using my::for_each; // ensures that the appropriate
using std::for_each; // version of for_each() is used
对于大多数迭代只需要一次比较,并且没有分支(请注意,分支可能会对性能产生严重影响)。请注意,我们不需要在命名空间std
中定义它(这可能是非法的),但可以确保适当的using
指令使用正确的版本。对于某些用户定义类型专门设置using std::swap;
时,这相当于swap()
。
答案 1 :(得分:7)
关于性能,您的for
循环会反复调用std::end
,而std::for_each
则不会。这可能会也可能不会导致性能差异,具体取决于所使用的容器。
答案 2 :(得分:4)
std::for_each
版本将只访问每个元素一次。阅读代码的人可以在看到std::for_each
后立即知道,因为在lambda中没有任何东西可以用来处理迭代器。在传统的for循环中,你必须研究循环的主体以获得异常的控制流(continue
,break
,return
)并使用迭代器进行dinking(例如,在这种情况下,使用++it
跳过下一个元素。
您可以在lambda解决方案中轻松更改算法。例如,您可以创建一个访问每个第n个元素的算法。在许多情况下,你并不是真的想要一个for循环,而是一个不同的算法,如copy_if
。使用算法+ lambda,通常更容易改变,并且更简洁。
另一方面,程序员更习惯于传统的for循环,因此他们可能会发现算法+ lambda更难阅读。
答案 3 :(得分:0)
首先,我看不出这两者之间有多大区别,因为for_each是使用for循环实现的。但请注意,for_each是一个具有返回值的函数。
其次,在这种情况下我将使用范围循环语法,因为无论如何这一天很快就会到来。
答案 4 :(得分:0)
实际上;在使用Lambda表达式的情况下,您必须声明参数类型和名称,因此不会赢得任何东西。
但是只要你想用它调用一个(命名的)函数或函数对象,它就会很棒。 (请记住,您可以通过std::bind
组合类似函数的内容。)
Scott Meyers的书(我相信它是Effective STL)描述了这样的编程风格非常好和清晰。