Klaus Kreft and Angelika Langer
http://www.cuj.com/experts/1902/langer.htm?topic=experts
Note: Article updated on January 5, 2001
for_each()和transform()的区别
泛型算法for_each()和transform()常被认为非常相似,都是将一个运算(以functor的形式提供)应用到输入区间内的每一个元素上。区别是for_each()忽略运算的返回值,而transform()将返回值拷贝到输出区间的元素上。这种理解是很常见的过度单纯的想法。然而,依照标准,两个算法之间的区别是根本性的。专栏的这个部分的目的是解释两个算法在概念上的区别,并指出想当然的理解造成的潜在可移植性问题。
阅读标准
我们进入实质性讨论之前,让了我们看一下标准怎么说的[注1]。
FOR_EACH。for_each()算法被描述于标准的非变动性运算(non-modifying sequence operation)一节。
template
Function for_each(InputIterator first, InputIterator last, Function f);
l
效果:将f作用在区间[first, last)中的每一个iterator的反引用上,从first开始,进行到last - 1。
l
返回值:f
l
复杂度:应用f精确的last - first次。
l
附注:如果f有返回值,此返回值被忽略。
TRANSFORM。transform()算法描述于标准的变动性算法(mutating sequence operation)一节。它有两个版本,一个版本(unary transform)工作在一个输入序列上,另一个版本 (binary transform)接受两个输入序列。既然我们想比较for_each()和transform(),我们只考虑unary transform。
template
OutputIterator transform(InputIterator first, InputIterator last,
OutputIterator result, UnaryOperation op);
l
效果:通过区间[resutl, result + (last - first))中的每个选择子i赋一个新值,此值等于op(*(first + (i - result))。
l
要求:op应该没有任何副作用[注2]。
l
返回值:result + (last - first)
l
复杂度:精确的last - first次应用op。
l
附注:result可以等于first。
的确,unary transform算法和for_each()算法都将一个一元运算作用在输入区间的每一个元素身上,并且是精确的一次。除此之外,他们几乎没有共同点。区别包括:
l
for_each()是非变动性算法;transform()是变动性算法。
l
for_each()忽略运算的返回值;transform()将返回值赋给输出区间中的连续元素。
l
for_each()返回functor的一个拷贝;transform()返回输出区间的end的iterator。
l
for_each()以一个确定的顺序应用运算,也就是从输入区间的开始进行到输入区间结束;transform()没有给出这样的承诺。
l
传给transform()的运算必须没有任何副作用;对传给for_each()的运算没有这样的限制。
让我们看看这些区别意味着什么,以及它们为什么存在。
意图
当使用一个泛型算法时,我们期望这个算法的有某个效果;否则调用就没有意义了。典型的效果包括产生一个返回值和序列中的元素被修改。
返回值。泛型算法产生的典型返回值是布尔值(比如includes())、计数值(比如count_if())、指向输入序列的某个特定元素的iterator(比如find_if())、指向输出序列的end的iterator(比如copy()),或表示一个区间的一对iterator(比如equal_range())。绝大多数泛型算法产生返回值,只有很少一些没有(比如,fill(),replace(),sort(),和swap())。
根据对序列中元素的修改,标准将算法分成变动性算法(mutating或modifying)和非变动性算法(inspecting或non-modifying)。
MUTATORS。 remove()、replace()、copy()和sort()这样的算法主动地产生副作用,即修改序列中的元素。典型地,它们通过反引用itrator来赋给一个新值。比如,copy()将输入区间的元素赋值给输出区间的元素。如果被修改的序列就是输入序列,那么标准称之为in-place算法;如果修改的是输出区间,那么标准称之为拷贝算法。比如,replace_if()是一个in-place算法,而replace_copy_if()是一个拷贝算法。
INSPECTORS。相对地,非改动性算法,不对任何元素作赋值。非改动性算法的例子是find_if(),count_if()和search()。非改动性算法实际目的不是修改任何元素,而是产生返回值。
在这一个意义上,transform()是一个变动性拷贝算法,因为它修改元素,它将functor的结果赋给了输出区间中的元素;而for_each()是非变动性算法,因为它不对任何元素作赋值。
如前所述,非变动性算法的唯一目的产生一个返回值。for_each()是非变动性算法,并且它返回传给它的functor。有人可能想问:如果它不修改任何元素并返回它收到的东西,那么在什么场合对序列中的元素使用for_each()?for_each()究竟有效果吗?的确, for_each()根本不主动产生任何效果。调用for_each()的实际目的是传给它的functor在作用于每个元素时产生的效果。更精确地:functor可以产生修改输入区间的效果,也可以通过在被调用时修改自身而产生有用的结果。
因为这个理由,传给for_each()的运算并不对副作用作出限制;用一个没有副作用的functor调用for_each()完全没有意义。这和传给transform()的运算是根本上的区别,根据标准,传给transform()的运算必须没有任何副作用。并且,于此,我们看到了for_each()和transform()的根本性区别。for_each()依赖functor的副作用,而transform()自己产生效果并禁止functor产生任何副作用。
(WQ注:原文如此,下面几段颇似重复)
在这个意义上,transform()是一个改动性拷贝算法,因为它修改元素,它将functor的结果赋给输出区间中的元素,而for_each()是一个非改动性算法,因为它不对任何元素作赋值。
非变动性算法的唯一目的是产生一个返回值。for_each返回传给它的functor对象。严格来说,for_each()不主动产生任何副作用。调用for_each()目的是传给它的functor产生的效果。functor可以通过修改输入序列而产生效果,也可以通过被调用时修改自身而产生一个结果。
因为这个理由,传给for_each()的运算不限制副作用;这和传给transform()的运算不同,根据标准,传给transform()的运算必须没有任何副作用。这是for_each()与transform()间的根本性区别。for_each()依赖functor的副作用,而transform()自己产生效果并禁止functor产生任何副作用。
这个区别解释了为什么for_each()保证了调用functor的顺序和次数。当一个运算有副作用时,我们需要知道这个运算以怎样的频率和顺序被调用,因为它可能对调用的次数和顺序敏感。另一方面,transform()禁止它的functor有任何副作用,并且只保证调用的次数而没有对调用顺序作任何描述。
结论
让我们考虑标准给出的for_each()和transform()的描述的推论。它导出一个简单的概念:“只在对所调用的functor的返回值的处理上有不同的而非常相似的算法”在很多情况下造成了误解。
副作用
有副作用的functor可以传给for_each(),但不能传给transform()。标准的意图是for_each()没有带副作用的functor时就没有意义,而transform()不需要functor在返回值外提供任何副作用。依照标准,被传给for_each()的functor可以有任何副作用,而传给transform()的functor绝不能有任何副作用。实践中,两者都导致了令人惊奇的东西。
functor的副作用可以是无害的,比如向stream输出提示信息或修改自身的数据成员,而不干扰泛型算法本身生成的效果。尽管如此,这样一个functor不能传给transform(),因为它违背了标准的要求。
另一方面,常识是一个functor无论带什么样的副作用都不可能是免费的。functor产生的副作用绝不能干扰泛型算法的行为。举例来说,对无效的itrator或序列使用泛型算法无论如何都是灾难性的。甚至传给for_each()的functor也必须服从这个公认的规则,即使标准没有这么说。
调用顺序
对调用顺序敏感的functor可以传给for_each(),但传给transform()就没道理了。标准没有对transform()算法调用functor的顺序作任何描述。因为这个原因,传给transform()顺序敏感的运算,其结果不可预知,而对for_each()则有明确定义。
为什么这在实践中造成问题?好吧,让我们研究一个例子。
具体例子
假设我们有下列情形:我们有一个字典,包含了名字和相关的信息,用map容器来实现的。此外,有一文件包含了一连串的名字。所有出现在此文件中的名字,它的对应条目必须从字典中移除。我们如何解决这个问题?
第一个主意可能是使用泛型算法remove_if()和remove_copy_if():将名字出现在文件中的条目从map中移除(并拷贝入另外一个map)。这当然不能工作,因为remove_if()和remove_copy_if()是变动性算法,它们试图通过反引用iterator而给输入序列中的元素赋值。然而,map容器不允许对它容纳的元素赋值;它的元素是const的key和对应的value的pair,const的key不能被改变。由于这个原因,试图对map使用remove_if()或remove_copy()_if的程序不能被编译。取代使用remove_if()和remove_copy_if(),map中的元素可以通过erase()成员函数而被更好地移除。
使用for_each()
让我们使用另外一种方法,使用for_each():对每个在文件中的名字,应用一个函数来删除map中的一个条目。functor可以看起来像这样:
template
class eraseFct {
public:
eraseFct(MapT* m) : theMap(m) {}
void operator() (string nam)
{ typename MapT::iterator iter = theMap->find(nam);
if (iter == theMap->end())
throw invalid_argument(nam);
theMap->erase(iter);
}
private:
MapT* theMap;
};
template
eraseFct eraser(MapT* m)
{ return eraseFct(m); }
functor可能被这样使用:
map directory_1;
// ... populate directory_1 ...
ifstream infile("toBeErased.txt");
for_each(istream_iterator(infile),istream_iterator(),
eraser(&directory_1));
在for_each()上使用functor很好,并且有期望的效果。functor的副作用是修改数据成员theMap所指向的map。注意,副作用不是顺序敏感的,于是关于调用顺序的保证不是必须的。另外,副作用不影响泛型算法的行为,因为functor不试图修改输入或输出iterator或序列。
到现在为止,都很好。现在,想像一下情形发生稍微的变化:不是从字典中移除条目,我们现在是分解字典;也就是说,必须将名字出现在文件中条目从现在的字典移入另外一个字典。我们如何解决这个新问题?
使用transform()
第一个直觉上的主意是用一个和传给for_each()的functor非常相似的functor来应用tranform():对每个出现在文件中的名字,应用一个函数来删除map中的一个条目,并返回这个条目以存入另外一个map。
我们稍微修改了最初的functor以能用于fransform()。主要的区别是修改后的functor返回被移除的元素的值,于是transform()能够将这个值存入输出序列。所有必须的修改在下面的实现中作了标记:号:
template
class eraseFct {
public:
eraseFct(MapT* m) : theMap(m) {}
typename MapT::value_type operator() (string nam)
{ typename MapT::iterator iter = theMap->find(nam);
if (iter == theMap->end())
throw invalid_argument(nam);
typename MapT::value_type res = *iter;
theMap->erase(iter);
return res;
}
private:
MapT* theMap;
};
template
eraseFct eraser(MapT* m)
{ return eraseFct(m); }
可以这样使用:
map directory_2;
transform(istream_iterator(infile),istream_iterator(),
inserter(directory_2,directory_2.end()),
eraser(&directory_1));
我们也可以在使用最初的functor的地方使用它,以解决开始时的问题,即移除条目:
map directory_1;
// ... populate directory_1 ...
ifstream infile("toBeErased.txt");
for_each(istream_iterator(infile),istream_iterator(),
eraser(&directory_1));
在for_each()上使用修改后的functor没有问题,并和最初的functor同样好地解决了开始时的问题。for_each()简单地忽略了functor的返回值,效果和最初的functor相同。
对transform()来说,情形令人惊讶地不同。传给tranform()修改后的functor,其行为既不是可预知的也不是可移植的,因为标准只允许对transform()使用没有副作用的functor,而我们的functor有副作用,即移除了它的数据成员所指向的map的一个元素。
这里,我们看到了for_each()和transform()之间的根本性区别。描述两个算法非常相似(唯有的区别是for_each()忽略functor的返回值而transform()不忽略),这是一个误导。实际上,两个算法使用完全不同的范畴的functor:一个是有副作用的;另外一个是没有副作用的。
理论vs.实践
标准禁止联合使用带副作用的functor和transform()。原因是标准想给予运行库的实现者可能的优化的自由。要求transformator不能有任何副作用是个非常强的要求。transformator被允许做事不多。它不能够修改它自己的数据成员;它不能够修改临时变量(it cannot modify temporaries);它不能够调用任何有副作用的函数(比如向一个流写入);它甚至不能重取volatile的变量的值。它能做全部就是检查它的参数和其它const的非volatile的字段,调用没有副作用的函数和产生一个返回值。在这些限制之下,运行库的实现者能够安全地应用优化。
一个可能的优化是在并发线程中执行transform()。没有副作用的functor是并发安全的;因为它不会导致运行环境的任何变化,如果在多个线程中并发调用functor也不会有任何潜在的冲突。这样一个优化过的transform()的无疑会打破我们的例子。
我们例子中的transformator可能删除一个map中的一个元素,而这不是原子操作。一个线程可能正在执行删除元素动作而另外一个线程正在检查map的end(the other checks for the end of the map),这个值在稍后会被第一个线程置为无效,结果第二个线程将会崩溃。这是个竞争状态,它起源于我们的transformator 违犯它不能有副作用的要求的事实。
实践中,你将会发现,对于绝大部分标准运行库的实作,传给transform()一个有副作用的fucntor工作得很好,并且产生了预期的和可靠的结果。事实上,我们所知的运行库的实作,没有一个使用了标准给予的优化的自由。尽管如此,谨记:严格来说,使用有副作用的transformator是不可移植的。
于是,在可移植的程序中,我们能用什么来替代使用transform()?立即,我们看到两个可能的方法:实现放松的transform()版本并用它取代标准的transform()泛型算法,或者使用for_each()替代。
实现自己的transform()版本
我们可以实现自己的transform()版本,它从头部开始调用functor并进行到end,允许functor有任意的副作用(除了使得输入或输出iterator或序列变无效的副作用)。这是一个可能的实现:
template
OutputIterator relaxed_transform(InputIterator first, InputIterator last,
OutputIterator result, Transformator trans) {
for ( ; first != last; ++first, ++result)
*result = trans(*first);
return result;
}
这是你在绝大部分标准运行库实作中发现的实现方式,但是使用你自己的版本更加安全,因为它是可移植的解决方案。上面的算法可以被申明为:
template
OutputIterator relaxed_transform(InputIterator first, InputIterator last,
OutputIterator result, Transformator trans);
l
效果:将trans应用在区间[first, last)内的每个iterator的反引用上,从first开始执行到last - 1,并将trans(*(first + (i - result))的返回值通过区间[result, result + (last - first))内的每个iterator进行赋值。
l
要求:trans没有任何导致区间[first, last)和[result, result + (last - first))内的iterator无效的副作用。
l
返回值:result + (last - first)
l
复杂度:精确的last - first次应用trans和精确的last - first次赋值。
l
附注:result可以等于first。
在result等于first的情况下,也就是说,输入序列和输出序列相同时,transform()算法被用作in-place算法。在这种情况下,使用者必须要注意对输入元素通过functor作的任何修改都会被随后的对此元素的赋值动作覆盖掉。这引入了一个小小的陷阱,但是使用修改输入元素的functor的用户可能无论如何也不会将这样一个functor用在in-place的transform()上。
使用自定义的放松的relaxed_transform()算法的目的和好处是容易实现可移植的程序。缺点是可能的优化在这个放松的自定义版本中不存在了。
在拿不准时就使用for_each()
另外一个可选方法是每当需要产生副作用时,使用for_each()算法。我们可以重新实现functor,以产生所有想要的副作用,包括transform()已经产生的;也就是说,它从字典中移除一个条目并将其加入另外一个字典。这里是重写的fuctor:
emplate
lass moveFct {
ublic:
moveFct (MapT* m1, MapT* m2) : theMap1(m1), theMap2(m2) {}
void operator() (string nam)
{ typename MapT::iterator iter = theMap1->find(nam);
if (iter == theMap1->end())
throw invalid_argument(nam);
theMap2->insert(*iter);
theMap1->erase(iter);
}
rivate:
MapT* theMap1;
MapT* theMap2;
;
emplate
oveFct mover(MapT* m1,MapT* m2)
return moveFct(m1,m2); }
可以被这样使用:
map directory_1;
// ... populate directory_1 ...
ifstream infile("toBeErased.txt");
for_each(istream_iterator(infile),istream_iterator(),
eraser(&directory_1));
这解决方案达到了推荐中的高度:我们通常应该用for_each()代替transform()来进行所有顺序敏感或有副作用的变换。
总结
这是个常见的误解:泛型算法for_each()和transform()之间的唯一区别是for_each()忽略运算的返回值而transform()将返回值拷贝到输出区间的元素中。两个算法之间的一个比较根本的区别是transform()被限制为使用无副作用的functor,而for_each()对它的functor的要求松得多。
事实上,for_each()是标准运行库的泛型算法中的一个例外:它是唯一一个承诺了调用functor的顺序和次数的算法,并允许副作用(包括修改输入序列中的元素)。详细来说:
l
for_each()是标准运行库中承诺了调用functor的顺序和次数的少有几个算法之一[注3]。这允许了通过functor执行顺序敏感功能的程序。将顺序敏感的functor传给其它任何算法是完全没意义的,因为结果不可预知。
l
for_each()是返回functor的唯一一个算法。这允许了在functor中累积数据成员信息并在执行完算法后重新获得此信息的程序。将这样的functor传给其它算法要求这些算法的实例是使用的functor的引用,这有些难度,因为它涉及显式函数实参申明(WQ注,参看《C++标准程序库》P298,形式为transform<InputIterator, InputIterator, OutputIterator, Transformator &>(...)),加上你将要和运行库的缺陷作斗争[注4]。
l
for_each()是少有几个不限制fuctor的副作用的算法之一[注5]。这给了用户实现functor时的巨大的灵活性。其它的所有算法都对所使用的functor有各种强制要求。
在本专栏的下一篇,我们将会讨论unary predicate,这是标准运行库中使用的另外一个范畴的functor。我们将看到这些functor应该有什么副作用或不应该有什么副作用。
注释和引用
[1] INTERNATIONAL STANDARD. Programming languages &151; C++. ISO/IEC IS 14882:1998(E).
[2] The Standard defines a side effect as follows: Accessing an object designated by a volatile lvalue, modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.
[3] The other algorithms that give a guarantee regarding the order of invocation are the generalized numeric operations accumulate, inner_product, partial_sum, and adjacent_difference defined in header file . These algorithms, different from for_each, require side-effect-free function objects.
[4] Klaus Kreft and Angelika Langer. "Effective Standard C++ Library: Explicit Function Template Argument Specification and the STL,"
C++Experts Forum
.
[5]The other algorithm that does not restrict the side effects of its function object is generate