1. 程式人生 > >函式配接器-摘自《C++沉思錄》Andrew Koenig

函式配接器-摘自《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)

              ;

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

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

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