CUJ:高效使用標準庫:STL中的unary predicate (轉)

amyz發表於2007-08-14
CUJ:高效使用標準庫:STL中的unary predicate (轉)[@more@]

Effective Standard C++ Library: Unary Predicates in the STL:namespace prefix = o ns = "urn:schemas--com::office" />

  Klaus Kreft and Angelika Langer

http://www.cuj.com/experts/1904/toc.htm?topic=experts


  標準執行庫中的幾個泛型演算法在執行時使用了一元判定式(unary predicate)。例子是帶if的演算法,比如count_if()、find_if()、remove_if()、和replace_if(),但也有partition()這樣[不帶if]的演算法。在本次專欄中,我們就近距離接觸unary predicate,看它們可能以及絕不能做什麼。

  讓我們看看標準如何定義unary predicate的。它在標準中被簡稱為predicate[注1]。

UNARY PREDICATE。

  Predicate引數被用於每當泛型演算法期望一個functor作用在相應的iterator的反引用上,並返回一個可以與true進行測試的值的時候。換句話說,如果一個泛型演算法接受一個predicate引數pred和iterator引數first,在構造中,它應該能正確工作: (pred(*first)){...}。

  functorpred不應該在iterator的反引用上應用任何非const函式。

  這個functor可以是一個指向函式的指標,或有合適的操作operator()的型別的物件。

  從這個描述和對使用unary predicate的泛型演算法進行的檢查(我們將在本文後面看到),我們能鑑別出unary predicate的很多典型特性。我們將在本文仔細討論每個特性。特性是:

  基本特性

1.  unary predicate必須是可呼叫的。

2.  unary predicate必須接受一個引數,並返回一個可轉換到布林型的值。

3.  unary predicate不需要可複製(copyable)。

  副作用特性

4.  unary predicate不能修改它的實參。

5.  unary predicate不能使泛型演算法正在存在的序列或iterator無效。

6.  unary predicate可以有4和5以外的任何副作用。

  其它特性

7.  unary predicate必須是順序不敏感的,這意味著呼叫predicate的效果必須不依賴於傳給它元素的順序。

8.  unary predicate不必對相同的實參的不同呼叫產生相同的結果。

  讓我們為什麼有道理讓predicate精確地擁有這些特性。

基本特性1、2和3

  unary predicate必須是可呼叫的,但不必可複製,並且必須接受一個引數和返回一個布林值。

  當我們考察標準如何定義在泛型演算法中使用unary predicate(predicate“在建構函式中,它應該正確工作: (pred(*first)){...})時,這些特性多少有些顯然。這裡是示範unary predicate的使用的泛型演算法的典型實現:

template

typename iterator_traits::difference_type

count_if(InputIterator first, InputIterator last, Predicate pred) {

  typename iterator_traits::difference_type n = 0;

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

  if ( pred(*first) )

  ++n;

  return n;

}

  換句話說,unary predicate像函式一樣被呼叫。“可呼叫的”的要求被指向函式的指標滿足,也被具有呼叫操作的型別的物件(所謂的functor)(或指向這樣的物件的引用)滿足。predicate被呼叫時,傳入一個引數。這個引數是一個iterator的反引用的結果,也就是對序列中元素的一個引用。返回值被用作條件,必須能轉換到布林型別。這是對unary prediate的意圖的完整描述:它被呼叫,以根據序列中的元素產生一個布林結果。

  特別地,對unary predicate沒有複製語義的要求。根本不需要可複製。作為一個通用規則,泛型演算法不能依賴於任何它所使用的物件沒被明確要求的特性。這包括泛型演算法絕不能複製predicate,因為沒被要求為他的predicate提供任何合理的複製語義。如果能將複製建構函式和賦值操作申明為私有成員並透過引用傳遞predicaet物件,就太好了。它不應該打破任何泛型演算法的。實踐中,你將發現假設predicate為可複製的執行庫,雖然它們不應該這麼做的。這樣的標準執行庫實作的一個令人驚訝的效果已經在C++ Report的一篇文章中被討論過了[注2]。同時一些執行庫實作已經除去了這個限制並且有期望中的行為;舉例來說,Metrowerks CodeWarrior 6.0。考慮到不同的執行庫實作,我們只能說最好避免具有“有趣的”複製語義或沒有複製語義的unary predicate。

  實踐中,絕大多數predicate擁有正常的複製語義。這是因為通常我們以predicate被傳值的方式來呼叫泛型演算法的。為了能這麼做,predicate必須是可複製的。不可複製的predicate可能有用,但不常見,因為它們必須被傳引用,這必須額外小心,並需要看起來很有趣的模板語法。我們在前一篇中進行了functor的傳引用問題的相關討論:我們如何完成它,以及我們為什麼可能想這麼做[注3]。於此就不再進一步討論了。而讓我們繼續進行到predicate的其餘特性,討論unary predicate產生的副作用。

副作用特性4、5和6

  unary predicate可以有任何副作用,除了修改它的實參和使得泛型演算法正在操作的序列或iterator無效。

  標準禁止這幾個副作用,但允許其它的。為什麼?為了理解這個要求,考慮一下在使用unary predicate的泛型演算法內部發生了什麼。有兩個產生副作用的實體:泛型演算法本身和unary predicate。泛型演算法遍歷輸入的元素序列,檢查元素,把它們作為引數傳給unary predicate,修改和複製它們,並且可能產生其它副作用。unary predicate接受一個元素的引用,同樣可能檢查和修改這個元素併產生其它副作用。自然地,這些行為可能互相沖突。因為這個理由,讓我們看一看unary predicate產生的副作用,並且根據潛在的衝突性分類為有害的、可能有害的和無害的副作用。

有害的副作用

  有害的副作用導致泛型演算法正在操作的序列或iterator無效。(functor絕不該產生有害的副作用。這是適用於所有functor的通用規則,而不只是對predicate。標準甚至沒有顯式禁止這樣的副作用,可能是因為這被認為是“常識”。)

  有害的predicate的一個例子是在predicate內部儲存了一個指向泛型演算法正在操作的序列中的元素的指標或引用,並使用這個[指標或]引用來刪除元素。元素的移除可能導致提供給泛型演算法(以指明輸入或輸出序列)的iterator無效,而且在這種情況下,泛型演算法可能導致崩潰。

  移除元素是一個非常顯眼的故障源,但有時,導致序列的無效並不怎麼明顯。如果泛型演算法的例項依賴於序列的排列順序,而predicate在被呼叫時有意或無意中修改了排序順序,那麼這將導致不可預知的結果。

  無論如何,具有有害副作用的predicate必須絕對避免。作為一個規則,絕不要使用任何導致泛型演算法所操作的序列或iterator無效的functor。

可能有害的副作用

  這種型別的副作用被標準顯式禁止。所有使用其引數的非cosnt函式的unary predicate都屬於此列,因為它們修改序列中的元素。讓我們稱之為變動性unary predicate。(注意“修改序列”(有害的)和“修改序列中元素”(只是可能有害)間的區別:“修改序列”意味著有元素插入或移除或移動,以使得某個iterator或iterator區間變得無效。“修改序列中的元件”意味著元素被訪問了,它們的內容被改變了,但是不會導致任何iterator無效。)

  變動性unary predicate的可能害處源於這樣的事實:predicate不是唯一一個訪問並改變輸入序列中的元素的東西。泛型演算法本身可能試圖修改相同的元素。在這種情況下,有兩個副作用實體(泛型演算法和predicate),它們的行為可能衝突。

  這樣的衝突何時發生?不是所有的泛型演算法都修改輸入序列的元素,但是有一些是這麼做了。泛型演算法分為幾類:非變動性演算法和變動性演算法,而變動性演算法又可以分為in-place演算法和複製演算法。非變動性演算法(比如,count_if())只是檢視元素;它們不作任何改變。變動性複製演算法(比如,replace_copy_if())不修改輸入序列的元素,但將它們複製入輸出序列;它們修改輸出序列的元素。變動性in-place演算法(比如,replace_if())“就地”修改元素,這意味著它們修改輸入序列的元素;他們是危險的東西。因此,predicate與泛型演算法間的潛在衝突就發生在同時使用變動性in-place演算法和變動性unary predicate的時候。

  對相同元素修改兩次的程式會導致兩個問題:哪個修改被先並可能被第二個修改覆蓋?結果可以完全預知嗎?為了避免泛型演算法和predicate間的這種衝突,標準要求unary predicate絕不能修改作為引數傳給它的輸入序列中的元素。注意這個變動性副作用對所有unary predicate都是禁止的,而不只是那些傳給變動性in-place演算法的unary predicate。

  這個限制經常反映在predicate的函式簽名中:典型地,一個unary predicate接受的引數不是傳值就是傳const的引用,以確保實參(相關的輸入序列的元素)不被修改。

無害的副作用

  最後,但是不最少,predicate可以具有無害的副作用。所有的對序列中元素的非變動性訪問都屬於這個範疇。predicate可以使用其引數的const的函式;也就是說,它可以檢視元素,但不能修改它們。另外,unary predicate可以修改實參以外的物件。比如,它可能擁有資料成員,並改變它們的值。或者它可能涉及無關元素序列並修改它們。只要被predicate改變的序列不是演算法演算法正在操作的序列,它就是無害的。

我們為什麼會關心?

  我們已經看到predicate可能產生的不同型別的副作用,並且許多副作用被標準禁止。為什麼我們會想在這些環境下使用帶副作用的predicate?帶副作用的predicate很少見還是很常見?

  嗯,視情況而定。如果你檢視C++教科書上predicate的例子,將發現isEven這樣的predicate被定義為bool isEven(int elem){return elem % 2 == 0;}或bind2nd(greater(),4)(用被稱為binder的東西從預定義的binary predicate產生的unary predicate)。即使你研究那些用functor型別(也就是,過載了呼叫操作的類)實現的unary predicate,他們也很少擁有資料成員或做複雜的事,而且從不具有副作用。

  實踐中,微有不同。舉例來說,我們關心。明顯地,在完成某些操作時,一次遍歷序列而不是重複在很長的序列上步進,會更快。考慮一些例子。

  假設,我們有一個容器來描述客戶。我們為內部統計的需要而檢測常客的數目,並且,我們想建立一個郵寄列表,因為我們想寄推銷信給常客。然而,郵寄列表不該超過5,000的界限。那是任務。我們如何完成?一個可能的方法是用一個unary predicate,對常客產生true,並由它累積郵寄列表的資訊。當將它傳給count_if()演算法時,將產生想要的計數並(作為副作用)建立郵寄列表。這樣的unary predicate嚴格地遵照了規則。 它接受一個對客戶的const的引用,檢視客戶,並且產生一個無害的副作用:郵寄列表。

  讓我們考慮另外一個相似的例子。我們需要將所有非常客從客戶資料中移除,並保留的常客的折扣記錄。再一次,我們試圖高效,而期望在一次遍歷客戶資料時將兩者都做掉。和前面的方法相比,我們可以試圖給remove_if()提供一個unary predicate,它對非常客產生true(以便泛型演算法移除它們),並將折扣的資訊加給其餘的客戶。 與較早的例子相反,這是不合法的,對輸入系列中的元素增加資訊是被禁止的副作用。記住:predicate絕不能修改它的實參。但那確實是我們想要的:我們想更新系列中的剩餘元素。那麼,怎麼做?

  我們沒有太多能做的。對標準的泛型演算法章節的徹底研究表明,for_each()是唯一一個接受functor,並允許functor修改實參的泛型演算法。(我們在前一篇文章討論了for_each()4)。) 因為這個原因,對於包含修改輸入系列的元素的任務,我們在泛型演算法的選擇上嚴重受限;for_each()基本上是唯一的選擇。

  結果是我們必須要將任務分解為非改動性的行為(實現為供remove_if()用的unary predicate) 和改動性的行為(實現為供for_each()用的functor)。這樣的對序列的額外遍歷是不可避免的,包括不可避免的效率損失。

  替代選擇是:

l  供for_each()使用的一個functor,移除和修改元素(重複了使用我們的predicate的remove_if()的功能,這當然不是我們想要的)

l  使用者自定義的remove_if()版本,它允許修改輸入序列的元素(這是可行的;甚至是標準泛型演算法的複製都可能仰賴於它是如何實現的)

l  手工編寫的演算法(忽略所有標準泛型演算法)

  底線是:如果輸入序列的元件一定要被修改,就不能使用unary predicate。

  如果需要這樣的修改(比如,因為效率的原因),而又想使用標準泛型演算法的話,那麼我們必須將任務分解為改動性和非改動性兩類,並且必須接受對輸入序列的多次遍歷。

特性 7

  unary predicate必須是順序不敏感的;也就是說,它的效果必須不依賴於呼叫順序。

  另外兩個方面和unary predicate的副作用有聯絡: predicate的呼叫順序和呼叫次數。如果每當predicate作用在輸入序列的元素上時都產生副作用的話,那麼我們會想知道副作用是以怎樣的頻度和順序產生的。舉例來說,在我們的例子中,我們累加一個計數以決定生成的郵寄列表的最大大小。自然地,predicate精確地對每個元素作用一次和可能重複作用,這之間是有區別的。依賴於副作用的性質,呼叫的順序和次數將扮演不同的角色。

  呼叫的次數被精確描述了:count_if()或remove_if()這類泛型演算法精確地將predicate作為於輸入序列中的每個元素一次。呼叫順序則有不同:沒有任何一個接受redicate的泛型演算法描述了它向predicate提供元素的順序。於是,unary predicate必須不依賴於呼叫順序。如果我們使用一個依賴於呼叫順序的predicate,結果不可預知。

  這裡是一個例子:一個(順序敏感的)predicate對序列中每第n個元素產生ture:

class Nth {

public:

  Nth(int n) : theN(n), theCnt(0) {}

  bool operator()(int)

  { return (++theCnt)%theN; }

private:

  const int theN;

  int theCnt;

};

  如果我們將Nth(3)這樣的predicate傳給remove_copy_if()這樣的泛型演算法,然後期望它會將輸入序列中的第3的倍數個的元素移入輸出序列。但這不能得到保證,因為序列中的元素不一定以明確的順序被操作。我們能確定的只是有三分之一的元素被從輸入序列移入輸出序列。

  為什麼標準沒有對unary predicate的呼叫順序給出保證?這是因為某些泛型演算法能夠對某些型別的iterator進行。舉例來說,如果 iterator是input iterator ,泛型演算法可能從序列的begin步進到end,但對ran access iterator能夠作任意跳進。因為標準不想限制這種最佳化的可能,所以沒有對unary predicate的呼叫順序給出保證。

  結果,對STL的使用者而言,所有的unary predicate都絕不能依賴於序列中的元素被提供的順序。如果我們想使用順序敏感的predicate,那麼就必須實現我們自定義的泛型演算法,以給予對unary predicate的呼叫順序的保證。

  關於unary predicate的呼叫順序和次數,這是最後一個觀測。

特性8

  unary predicate在對相同實參的不同呼叫時,不必產生相同的結果。

  這個特性可能聽起來稍微牽強。我們將它列入特性表,因為我們注意到有時候存在一個假設,隱含要求predicate表現為“穩定的”行為,也就是說當它們被用相同或“相等/等價”的實參呼叫時,每次都產生相同的結果。這個假設不成立;標準沒有規定任何類似的要求。

  那麼,為什麼有時候假設unary predicate被要求有“穩定的”行為?因為它會給實作相當多的自由度。具有“穩定的”行為,它不必在意predicate被相同的元素呼叫了多少次,因為結果總是相同的。它也不在乎傳給泛型演算法的是序列中元素的引用還是其臨時複製(即“相等/等價”元素)。

  然而,對STL的unary predicate,沒有“穩定的”行為的要求。完全有道理定義一個“不穩定”的predicate。例如,對所有具有某個屬性的元素都產生true,直到達到一個界限。它可能被用於remove_copy_if()以從輸入序列中複製maximum個具有給定屬性的元素到輸出序列。

總結

  標準執行庫的下列泛型演算法使用unary predicate:replace_if(), remove_if(),partition(),stable_partition(),replace_copy_if(),remove_copy_if(),count_if()和find_if()。

  如果一個unary predicate具有下列特性,那麼它能用於上述任何泛型演算法,並且結果可移植和可預知。

l  unary predicate必須是可呼叫的(callable),並且必須接受一個實參,並返回一個布林值,但不必具有任何特別的複製語義。(某些執行庫實作對此有限制,因為它們需要某種複製語義。)

l  unary predicate絕不能修改它的實參,並且絕不能導致泛型演算法正在操作的序列或iterator無效,但可以有其它任何副作用。

l  unary predicate必須不依賴於呼叫順序,並且可以對相同實參的不同呼叫產生不同的結果。

引用

[1] International Standard. Programming languages — C++ ISO/IEC IS 14882:1998(E).

[2] Nicolai M. Josuttis. "Predicates vs. Function s," C++ Report, June 2000.

[3] Klaus Kreft and Angelika Langer. "Effective Standard C++ Library: Explicit Function Template Argument Specification and the STL," C/C++ Users Journal, December 2000, http://www.cuj.com/experts/1812/langer.html.

[4] Klaus Kreft and Angelika Langer. "Effective Standard C++ Library: for_each vs. tranorm," C/C++ Users Journal, February 2001. http://www.cuj.com/experts/1902/langer.html


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-956523/,如需轉載,請註明出處,否則將追究法律責任。

相關文章