函數配接器-摘自《C++沉思錄》Andrew Koenig

       第21章介紹了一個叫做 transform 的函數,它是標準庫的一部分。它對序列中的每個元素運用函數或者函數對象,並且可以獲得一個新的序列。這樣,如果 a 是一個有100個元素的數組,f 是一個函數,則

       transform(a, a + 100, a, f);

將對 a 的每個元素調用 f,並把結果存回到 a 的相應元素中。

       第21章還舉了一個例子,說明如何使用 transform 來定義一個讓數組的每個元素和一個整數 n 相加的函數對象:

class Add_an_integer {
public:
	Add_an_integer(int n): n(n0) {}
	int operator() const (int x) { return x + n; }

private:
	int n;
};

       我們可以把這些函數對象之一當作傳給 transform 的參數。

       transform(a, a + 100, a, Add_an_integer(n));

       爲了這個目的定義一個類有點小題大作,所以標準庫提供了用於簡化這項工作的類和函數。

 

1.   爲什麼是函數對象

       首先要記住的是,Add_an_integer 是函數對象類型,而不是函數類型。之所以要用函數對象,是因爲使用函數對象可以將一個函數和一個值捆綁到單個實體中。如果我們願意把 n 放到具有一個文件作用域(file scope)的變量中,我們本來也可以使用函數的:

       static int n;

       static int add_n(int x) { return x + n; }

       這樣使用文件作用域變量是非常令人不方便的,所以我們還是採用函數對象。

       函數對象的優點就在於它們是對象,這就意味着,原則上對別的對象可以做的事情,對它們一樣可以做。實際上,標準庫爲我們提供了所有需要的東西,使我們根本不必定義輔助函數或者對象就能獲得與 Add_an_integer 同樣的效果。要讓序列的所有元素都加 n,我們只需寫如下函數

       transform(a, a + 100, a, bind1st(plus<int>(), n));

看上去似乎不很明白,但是子表達式

       bind1st(plus<int>(), n)

使用標準庫創建了一個函數對象,該函數對象具有與

       Add_an_integer(n)

相同的,以後作爲 transform 的最後個參數所必需的屬性。

       那麼,這個子表達式是如何工作的呢?

 

2.   用於內建操作符的函數對象

       爲了理解表達式

       bind1st(plus<int>(), n)

我們從子表達式

       plus<int> ()

開始。這裏的 plus 表示的是一個類型,而不是一個函數,所以 plus<int>()是一個等價於類型爲 plus<int>的無名對象的表達式。這樣的對象就是把那些把兩個類型爲 int 的值相加,並以它們的和作爲結果的函數對象。所以,譬如如果我們有如下代碼

       plus<int> p;

       int k = p(3, 7);

則 k 被初始化爲值 10.類似的,我們可以定義

       int k = (plus<int>()) (3, 7);

該語句也令 k 爲值10。

       除了使 plus<int>() 成爲函數對象的 operator() 成員外,類 plus<int> 還有 3 個其他成員,它們是類型名。這 3 個類型成員分別是 first_argument_type、second_argument_type 以及 result_type;從它們的名字就可以知道它們的含義。比如說 plus<int>::first_argument_type 就是 int 的一個完全名稱。稍後我們會明白爲什麼說訪問這些類型會很有用處。

       標準庫包括內建操作符所需要的絕大多數函數對象。它們存在的原因是顯而易見的。C++沒有辦法在類似

       bind1st(plus<int>(), n)

的表達式中直接應用內建操作符+。

 

3.   綁定者(Binders)

       我們已經知道如何用標準庫創建一個將兩個值相加的函數對象;現在我們需要創建一個能夠記住一個值,並把該值加到它的(單個)參數上的函數對象兩個分別叫做 bind1st 和 bind2nd 的庫模板函數簡化了這項工作

       如果 f 是類似 plus 的函數對象,有一個接受兩個參數的 operator(),而且如果 x 是一個可以作爲 f 第一個參數的值,那麼

       bind1st(f, x)

將生成一個新的函數對象,該函數對象只接受一個參數,它有一種有趣的特性,就是

       (bind1st(f, x)) (y)

具有和

       f(x, y)

相同的值。取名爲 bind1st 是爲了表現該函數的特點:創建一個函數對象,該函數對象綁定了函數的第一個參數也就是說,調用 bind1st 後返回的函數對象記住了某個值,並把這個值作爲第一個參數提供給用戶調用的函數

       以下是 bind1st 的定義說明:

       (bind1st(plus<int>(), n)) (y)

等價於 n + y,這正是我們想要的。但是它是如何工作的?

       理解這樣一個表達式的最簡單的方法就是將它分成幾塊來分別研究。要這樣做我們可以編寫這樣的代碼:

       // p是一個將兩個整數相加的函數對象

       plus<int> p;

      

       // b是一個將參數加到 n 上去的函數對象

       some_type b = bind1st(p, n);

 

       // 初始化 z 爲 n + y

       int z = b(y);

       但是 b 的類型是什麼呢?

       獲得答案還需要另一個標準庫模板類型,叫做 binder1st。其第一個模板參數就是傳給 bind1st 的第一個參數的類型(也就是將要調用的函數或者函數對象)。也就是說,要聲明前面的 b,我們應該編寫語句:

       // p 是一個將兩個整數相加的函數對象

       plus<int> p;

 

       // b 是一個將參數加到 n 上去的函數對象

       binder1st<plus<int>> b = bind1st(p, n);

 

       // 初始化 z 爲 n + y

       int z = b(y);

       現在可以更容易看清楚發生了什麼事情了:和前面一樣,p 是一個函數對象,負責把兩個數相加; b 是一個函數對象,負責將 n 綁定在(被相加的)兩個數中的第一個數上,於是 z 就成爲 n + y 的結果

 

4.   更深入的探討

       假定我們來寫 binder1st 的聲明。起初是很簡單的。我們知道 binder1st 是個函數對象,所以需要一個 operator():

template <class T> class binder1st {
public:
	T1 operator() (T2);
	// ...
};

Here is our first problem: What are the right types for T1 and T2?

       When we call bind1st wight argument f and x, we want to get a funciton object that can be called with the second argument of f (the one that is not bound) and return a result that is the same type of the result of f. But how do we figure out what those types are? We tried that with function composition in Chapter 21 and saw how hard it was.

       Fortunately, our task is greatly simplified by the convention, mentioned in Section 22.2, that the relevant function objects have type members whose names are first_argument_type, second_argumnt_type, and result_type. 如果我們要求只有遵循這個約定的類才能使用 binder1st,我們就能很容易地爲 operator() 以及相同情況下的構造函數填寫類型:

template <class T> class binder1st {
public:
	binder1st(const T &, const T::first_argument_type &);
	T::result_type operator() (const T::second_argument_type*);
	// ...
};

       利用同一個約定,我們還可以這樣聲明 bind1st:

       template<class F, class T> binder1st<F> bind1st(const F &, const T &);

       關於 bind1st 和 binder1st 的定義留作讀者練習。

 

5.   接口繼承

       模板類 plus 是函數對象類家族的成員之一,這些類都定義了成員 first_argument_type、second_argument_type 和 result_type。只要我們的一些類都具有某些相同的特殊成員,就應該考慮把這些成員放到一個基類中。C++庫正是這樣做的。實際上,plus 有一個叫做 binary_funciton 的基類,定義如下:

template <class A1, class A2, class R> class binary_function {
public:
	typedef A1 first_argument_type;
	typedef A2 second_argument_type;
	typedef R result_type;
}

       它極大的方便了定義其他函數對象的工作。例如,我們可以這樣定義 plus:

template <class T> class plus: public binary_function<T, T, T> {
public:
	T operator() (const T & x, const T & y) const {
		return x + y;
	}
};

       除了包括類 binary_function 外,標準庫還有一個 unary_function 基類,定義如下:

template<class A, class R> class unary_function {
public:
	typedef A argument_type;
	typedef R result_type;
};

       比如,這個類可以當作 negate 的基類使用,negate 的對象對值執一元操作-:

template <class T> class negate: public unary_function<T, T> {
public:
	T operator() (const T & x) const {
		return -x;
	}
};

       還有很多類似的函數;在任何一本關於 STL 或者即將問世的 C++ 標準庫的書裏都能找到全部細節。

 

6.   使用這些類

       假設 c 是某種標準庫容器,x 是一個可以存放到容器中的值。那麼

       find(c.begin(), c.end(), x);

將生成一個指向 c 中第一個與 x 相等的元素的迭代器,如果不存在等於 x 的元素就獲得一個指向緊跟在容器尾部後的元素的迭代器。我們可以使用函數配接器以一種更精巧的方法來得到同樣的結果:

       find_if(c.begin(), c.end(), bind1st(equal_to<c::value_type>(), x))

       這裏,因爲我們想知道是否對於每個元素 e 都存在 e > x,所以採用 bind2nd,而不用別的方法。

       假設 x 和 w 都是容器 ,且具有相同個數的元素。那麼,我們就可以通過下面的代碼將 w 的每個元素與 v 中的相應元素相加:

       transform(v.begin(), v.end(), w.begin(), v.begin(), plus<v::value_type>());

       這裏,我們採用了具有 5 個參數的 transform.前兩個參數是用來限制區間範圍的迭代器;第三個參數是要和第一個區間大小相等的第二個區間的頭部。這個版本的 transform 依次獲得每個區間的元素,並用它們作爲參數來調用作爲 transform 的第5個參數,結果存放到由 transform 的第 4 個參數指定開始位置的序列中。本例中 transform 的第 5 個參數是一個函數對象,它將兩個類型爲 v::value_type 的值相加,並獲得一個相同類型的結果。

       更爲普遍的是,本例可以不侷限於數字;只要 v 和 w 的容器類型允許 + 操作,就可以對參數採用適當的 + 操作。

       標準庫包括能夠用普通構造函數對象的函數適配器。和上一個例子一樣,如果有類似

       char* p[N];

的字符指針數組,我們就可以找出每個指向包含 “C” 的以 null 結尾的字符串的指針,並用指向字符串 “C++” 的指針進行替換:

       replace_if(p, p + N, not1(bind2nd(ptr_fun(strcmp), "C")), "C++");

       本例使用了庫函數 replace_if。它的頭兩個參數限定了一個區間,第三個參數是判斷替不替換容器元素,第四個參數用於替換的值。

       第三個參數的判斷本身就涉及到 3 個函數適配器: not1、bind2nd 以及 ptr_fun。配接器 ptr_fun 創建了一個適合於傳遞給 strcmp 的函數對象。bind2nd 使用這個對象來創建另一個函數對象,新產生的函數對象將用 “C” 來和它的參數進行比較。對 not1 的定義否定了判斷的意義,如果它的參數相等,而 0 又被普遍當作 false 解釋,那麼這個否定對於適應 strcmp 返回 0 的情況是很有必要的。

 

7.   討論

       這種編程方式是不是很難理解?爲什麼每個人都要這樣編寫程序?

       原因之一是理解的難易程度總是和熟悉程度密切相關。大多數學習 C 和 C++ 的人都在某個時候遇到過這樣的問題:

       while ((*p++ = *q++) != 0)

              ;

       最初幾次看到這樣的代碼可能會困惑不解,但是很快概念的強化就會在心理上打開通道,以致理解這種水平的程序,反而比理解單獨的操作要容易。

       另外,這些程序不比相應的那些常規程序運行得慢。理論上它們可以更快:因爲這些函數配接器是標準庫的一部分,編譯器在適當的時候可以識別它們,並生成特別高效的代碼。

       這種編程方式使得一次處理整個容器成爲現實,而不必採用循環來逐個處理單個元素。這也使得程序更短小、更可靠,如果你熟悉了,就會覺得易於理解了。

 

 

 


     

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章