CUJ:高效使用标准库:for_each() vs. transform()

本文详细对比了C++泛型算法for_each()和transform()。二者虽都将运算应用到输入区间元素上,但区别显著。for_each()是非变动性算法,依赖functor副作用,保证调用顺序;transform()是变动性算法,禁止functor有副作用,不保证调用顺序。还给出替代方案。

Effective Standard C++ Library: for_each() vs. transform()

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_EACHfor_each()算法被描述于标准的非变动性运算(non-modifying sequence operation)一节。

template<class InputIterator, class Function>

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有返回值,此返回值被忽略。

TRANSFORMtransform()算法描述于标准的变动性算法(mutating sequence operation)一节。它有两个版本,一个版本(unary transform)工作在一个输入序列上,另一个版本 (binary transform)接受两个输入序列。既然我们想比较for_each()和transform(),我们只考虑unary transform。

template<class InputIterator, class OutputIterator, class UnaryOperation>

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 MapT>

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 <class MapT>

eraseFct<MapT> eraser(MapT* m)

{ return eraseFct<MapT>(m); }

functor可能被这样使用:

map<string,info> directory_1;

// ... populate directory_1 ...

ifstream infile("toBeErased.txt");

for_each(istream_iterator<string>(infile),istream_iterator<string>(),

         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 MapT>

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 <class MapT>

eraseFct<MapT> eraser(MapT* m)

{ return eraseFct<MapT>(m); }

    可以这样使用:

map<string,info> directory_2;

transform(istream_iterator<string>(infile),istream_iterator<string>(),

          inserter(directory_2,directory_2.end()),

          eraser(&directory_1));

    我们也可以在使用最初的functor的地方使用它,以解决开始时的问题,即移除条目:

map<string,info> directory_1;

// ... populate directory_1 ...

ifstream infile("toBeErased.txt");

for_each(istream_iterator<string>(infile),istream_iterator<string>(),

         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 <class InputIterator, class OutputIterator, class Transformator>

OutputIterator relaxed_transform(InputIterator first, InputIterator last,

                         OutputIterator result, Transformator trans) {

  for ( ; first != last; ++first, ++result)

    *result = trans(*first);

  return result;

}

    这是你在绝大部分标准运行库实作中发现的实现方式,但是使用你自己的版本更加安全,因为它是可移植的解决方案。上面的算法可以被申明为:

template<class InputIterator, class OutputIterator, class Transformator >

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 <class MapT>

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 <class MapT>

oveFct<MapT> mover(MapT* m1,MapT* m2)

 return moveFct<MapT>(m1,m2); }

    可以被这样使用:

map<string,info> directory_1;

// ... populate directory_1 ...

ifstream infile("toBeErased.txt");

for_each(istream_iterator<string>(infile),istream_iterator<string>(),

         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 <numerics>. 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.

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值