整理你的循环 ... 使用 for_each 替换 for






3.60/5 (21投票s)
2005年5月28日
6分钟阅读

69812
如何使用 STL for_each 算法实现循环,以及为什么这样做是个好主意。
引言
你读过多少次关于使用 STL 算法而不是手写循环的优点?很有可能你已经听过很多次了。问题是,在实际程序中真正实现这一点并不直观;特别是如果你是 STL 新手。然而,一旦你了解了 STL 算法如何与你的代码交互,使用 for_each
就会变得容易。事实上,它变得如此简单和强大,以至于你可能在你的职业生涯中再也不会写另一个循环了。
for_each 算法
for_each
算法遍历容器,并对容器中的每个元素“执行某些操作”。作为程序员,当你调用 for_each
算法时,你必须指定你希望对每个元素执行什么操作。你通过提供一个函数来做到这一点,该算法在遍历容器时会调用该函数。
这个算法不是魔术。它执行你认为它会执行的操作。所以让我们来看看 Microsoft C++ 7 中提供的 for_each
的实现
/* * Copyright (c) 1992-2002 by P.J. Plauger. ALL RIGHTS RESERVED. * Consult your license regarding permissions and restrictions. */ // TEMPLATE FUNCTION for_each template<class _InIt, class _Fn1> inline _Fn1 for_each(_InIt _First, _InIt _Last, _Fn1 _Func) { // perform function for each element for (; _First != _Last; ++_First) _Func(*_First); return (_Func); }
如你所见,该算法遍历传入的范围,并为范围内的每个元素调用一个函数。
用 for_each
替换循环的一般思路是,你编写一个实现循环逻辑的函数,也就是说,你希望对循环的每次迭代执行什么操作。然后你将这个函数作为第三个参数 (_Func
) 提供给 for_each
算法。听起来不错?嗯,恐怕事情没那么简单。
请注意,for_each
调用的函数签名是固定的。它必须是一个只接受一个参数的函数,并且该参数的类型必须与容器中元素的类型相同。因此,如果你编写一个实现循环逻辑的函数,它唯一可以拥有的参数就是循环中的当前元素;问题就出在这里。
如果你想有多个参数怎么办?
使用函数对象构建逻辑层次结构
让我们举一个具体的例子。假设我们正在遍历一个 <wstring>
向量,并且我们想将向量中的每个 <wstring>
与一个外部 wstring
对象进行比较,如果它们相同则将其写入文件。
不使用 for_each
,我们的代码将如下所示
wstring wstrTarget; wstring wstrFile; for ( vector<wstring>::iterator itr = vec.begin(); itr != vec.end(); itr++ ) { if ( *itr == wstrTarget ) writeToFile( *itr, wstrFile ); }
我们将如何使用 for_each
来做同样的事情呢?嗯,我们需要一个函数,它可以将当前元素与 wstrTarget
进行比较,如果它们匹配,则将当前元素写入名为 wstrFile
的文件。因此,该函数需要知道三件事:当前元素、要匹配的对象以及要写入的文件名。但是我们如何将这三件事告诉函数呢?请记住,我们只能向函数提供一个参数,即当前元素,那么我们如何将其他信息传入函数呢?
答案在于函数对象。
因为函数对象可以像函数一样被调用,所以我们可以将其用作传递给 for_each
的函数。也就是说,在上面给出的 for_each
实现中,_Func(*_First)
既可以调用函数对象,也可以调用函数。此外,因为函数对象是对象(像任何其他对象一样),它们可以拥有属性、方法和构造函数。这允许我们向函数对象提供执行循环逻辑所需的额外数据,这些数据不能由 for_each
算法自动提供。
一种方便简洁的方法是在函数对象的构造函数中提供额外的数据。
在我们的示例中,我们可以定义一个名为 compareAndWrite
的函数对象,它在其构造函数中接受目标 wstring
对象和要写入的文件名。定义一个接受 const wstring&
作为参数的 operator()
允许 for_each
算法使用该函数对象,并从每次迭代中接收当前元素。
将所有部分组合在一起,我们得到 compareAndWrite
函数对象的以下定义
struct compareAndWrite { const wstring& wstrTarget; const wstring& wstrFile; compareAndWrite( const wstring& wstrT, const wstring& wstrF ) : wstrTarget(wstrT), wstrFile(wstrF) { } void operator()( const wstring& val ) { if ( val == wstrTarget ) writeToFile( val, wstrFile ); } };
这允许我们用以下对 for_each
的调用替换手动编写的 for
循环。
for_each( vec.begin(), vec.end(), compareAndWrite(wstrTarget,wstrFile) );
为什么这更好?
好的,所以我们必须编写更多代码来使用函数对象,但是额外的代码是值得编写的,原因如下。
需要额外的代码来定义函数对象。我们必须定义类结构、方法和变量。一旦我们定义了类,实际的循环逻辑就真的没有太大不同了,并且由类的 operator()
定义。
那么,当需要更多代码时,为什么还要使用 for_each
算法呢?
那么,对于手写循环,你如何
- 在多个地方使用同一个循环?
- 拥有多个共享某些通用逻辑但执行不同操作的循环?
在没有函数对象的情况下重用循环不可避免地涉及创建循环代码的新副本。这不好。你的软件中现在有两个相同代码的副本,位于两个不同的部分。
构建稍微不同的循环版本通常也需要复制代码并修改循环逻辑。同样,你现在有两段执行相似操作但需要维护的代码块。
函数对象和 for_each
算法让你能够将循环逻辑构建成一个完整的类,并从中获得继承、多态和封装带来的所有好处。
你可以构建一个类层次结构来定义循环逻辑的不同版本,而无需维护相同代码的多个副本或通过参数化使循环逻辑复杂化(如果你在函数而不是函数对象中定义循环逻辑,就会出现这种情况)。
因此,在这个例子中,要使用 compareAndWrite
循环的一个稍微不同的版本,我们所需要做的就是定义 compareAndWrite
函数对象的一个子类,并在调用 for_each
时使用它。
struct multiCompareAndWrite : public compareAndWrite { // loop logic here }; for_each(vec.begin(), vec.end(), multiCompareAndWrite(wstrTarget,wstrFile));
其他好处
- 更简单的逻辑
在循环中,通常有变化的变量和保持不变的变量。使用函数对象和
for_each
技术允许程序员定义哪些变量在循环中保持不变,哪些变量不保持不变。循环逻辑所需的额外数据在函数对象的构造函数中提供。循环逻辑未更改的数据在函数对象中声明为const
。这样,任何阅读你代码的程序员都可以立即看到哪些数据被更改,哪些数据未被循环逻辑更改。 - 更易读
为了弄清楚手写的循环做了什么,你需要阅读代码。然而,命名良好的函数对象,如
compareAndWrite
、addRecToDatabase
等,其名称就传达了它们的作用。例如,以下语句的作用一目了然,无需任何解释或深入代码。
for_each(vec.begin(), vec.end(), addRecToDatabase(connectString));
摘要
用 STL 的 for_each
算法替换 for
循环是让面向对象编程为你工作的好方法。通过构建循环逻辑的类层次结构,你可以避免维护相同或相似代码的多个副本。更重要的是,你的代码变得更整洁、更易读、更组件化。
这种技术的关键点是
- 创建实现和封装循环逻辑的函数对象。
- 将循环逻辑所需的任何额外数据传入函数对象的构造函数。
- 如果任何额外的数据未被循环逻辑更改,则在函数对象中将其声明为
const
。 - 构建循环逻辑函数对象的类层次结构,以最大限度地提高软件的重用性和健壮性。