Boost.Bind用法詳解(二)

函數組合,Part II

在SGI STL中的另一個常用的函數組合是 compose1 ,在 Boost.Compose 中是compose_f_gx 。這些函數提供了用一個參數調用兩個函數的方法,把最裏面的函數返回的結果傳遞給第一個函數。有時一個例子勝過千言萬語,設想你需要對容器中的浮點數元素執行兩個算術操作。我們首先把值增加10%,然後再減少10%;這個例子對於少數工作在財政部門的人來說可能是有用的一課。

std::list<double> values;
values.push_back(10.0);
values.push_back(100.0);
values.push_back(1000.0);

std::transform(
  values.begin(),
  values.end(),
  values.begin(),
  boost::bind(
    std::multiplies<double>(),0.90,
    boost::bind<double>(
      std::multiplies<double>(),_1,1.10)));

std::copy(
  values.begin(),
  values.end(),
  std::ostream_iterator<double>(std::cout," "));

你怎麼知道哪個嵌套的 bind 先被調用呢?你也許已經注意到,總是最裏面的 bind 先被求值。這意味着我們可以把同樣的代碼寫得稍微有點不同。

std::transform(
  values.begin(),
  values.end(),
  values.begin(),
  boost::bind<double>(
    std::multiplies<double>(),
    boost::bind<double>(
      std::multiplies<double>(),_1,1.10),0.90));

這裏,我們改變了傳給 bind 的參數的順序,把第一個 bind 的參數加在了表達式的最後。雖然我不建議這樣做,但它對於理解參數如何傳遞給 bind 函數很有幫助。

bind 表達式中的是值語義還是指針語義?

當我們傳遞某種類型的實例給一個 bind 表達式時,它將被複制,除非我們顯式地告訴bind 不要複製它。要看我們怎麼做,這可能是至關重要的。爲了看一下在我們背後發生了什麼事情,我們創建一個 tracer 類,它可以告訴我們它什麼時候被缺省構造、被複制構造、被賦值,以及被析構。這樣,我們就可以很容易看到用不同的方式使用 bind 會如何影響我們傳送的實例。以下是完整的 tracer 類。

class tracer {
public:
  tracer() {
    std::cout << "tracer::tracer()\n";
  }

  tracer(const tracer& other) {
    std::cout << "tracer::tracer(const tracer& other)\n";
  }

  tracer& operator=(const tracer& other) {
    std::cout <<
      "tracer& tracer::operator=(const tracer& other)\n";
    return *this;
  }

  ~tracer() {
    std::cout << "tracer::~tracer()\n";
  }

  void print(const std::string& s) const {
    std::cout << s << '\n';
  }
};

我們把我們的 tracer 類用於一個普通的 bind 表達式,象下面這樣。

tracer t;
boost::bind(&tracer::print,t,_1)
  (std::string("I'm called on a copy of t\n"));

運行這段代碼將產生以下輸出,可以清楚地看到有很多拷貝產生。

tracer::tracer()
tracer::tracer(const tracer& other)
tracer::tracer(const tracer& other)
tracer::tracer(const tracer& other)
tracer::~tracer()
tracer::tracer(const tracer& other)
tracer::~tracer()
tracer::~tracer()
I'm called on a copy of t

tracer::~tracer()
tracer::~tracer()  // 譯註:原文沒有這一行,有誤

如果我們使用的對象的拷貝動作代價昂貴,我們也許就不能這樣用 bind 了。但是,拷貝還是有優點的。它意味着 bind 表達式以及由它所得到的綁定器不依賴於原始對象(在這裏是 t)的生存期,這通常正是想要的。要避免複製,我們必須告訴 bind 我們想傳遞引用而不是它所假定的傳值。我們要用 boost::ref 和 boost::cref (分別用於引用和 const 引用)來做到這一點,它們也是 Boost.Bind 庫的一部分。對我們的 tracer 類使用 boost::ref,測試代碼現在看起來象這樣:

tracer t;
boost::bind(&tracer::print,boost::ref(t),_1)(
  std::string("I'm called directly on t\n"));

Executing the code gives us this:

tracer::tracer()
I'm called directly on t

tracer::~tracer()  // 譯註:原文爲 tracer::~tracer,有誤

這正是我們要的,避免了無謂的複製。bind 表達式使用原始的實例,這意味着沒有tracer 對象的拷貝了。當然,它同時也意味着綁定器現在要依賴於 tracer 實例的生存期了。還有一種避免複製的方法;就是通過指針來傳遞參數而不是通過值來傳遞。

tracer t;
boost::bind(&tracer::print,&t,_1)(
  std::string("I'm called directly on t\n"));

因此說,bind 總是執行復制。如果你通過值來傳遞,對象將被複制,這可能對性能有害或者產生不必要的影響。爲了避免複製對象,你可以使用 boost::ref/boost::cref 或者使用指針語義。

虛擬函數也可以綁定

到目前爲止,我們看到了 bind如何可以用於非成員函數和非虛擬成員函數,但是它也可以用於綁定一個虛擬成員函數。通過 Boost.Bind, 你可以象使用非虛擬函數一樣使用虛擬函數,即把它綁定到最先聲明該成員函數爲虛擬的基類的那個虛擬函數上。這個綁定器就可以用於所有的派生類。如果你綁定到其它派生類,你就限制了可以使用這個綁定器的類[5]。考慮以下兩個類 base 和 derived :

[5] 這與聲明一個類指針來調用虛擬函數沒有什麼不同。指針指向的派生類越靠近底層,則越少的類可以綁定到指針。

class base {
public:
  virtual void print() const {
    std::cout << "I am base.\n";
  }
  virtual ~base() {}
};

class derived : public base {
public:
  void print() const {
    std::cout << "I am derived.\n";
  }
};

我們可以用這兩個類對綁定到虛擬函數進行測試,如下:

derived d;
base b;
boost::bind(&base::print,_1)(b);
boost::bind(&base::print,_1)(d);

運行這段代碼可以清楚地看到結果正是我們所希望的。

I am base.
I am derived.

對於可以支持虛擬函數,你應該不會驚訝,現在我們已經示範了它和其它函數一樣運行。有一個相關的注意事項,如果你 bind 了一個成員函數而後來它被一個派生類重新定義了,或者一個虛擬函數在基類中是公有的而在派生類中變成了私有的,那麼會發生什麼呢?還可以正常工作嗎?如果可以,你希望是哪一種行爲呢?是的,不管你是否使用 Boost.Bind,行爲都不會有變化。因面,如果你 bind到一個在其它類中被重新定義的函數,即它不是虛擬的並且派生類有一個相同特徵的成員函數,那麼基類中的版本將被調用。如果函數被隱藏,綁定器依然會被執行,因爲它顯式地訪問類型中的函數,這樣即使是被隱藏的成員函數也可以使用。最後,如果虛擬函數在基類中聲明爲公有的,但在派生類中變成了私有的,那麼對一個派生類實例調用該函數將會成功,因爲訪問是通過一個基類實例產生的,而基類的成員函數是公有的。當然,這種情況顯示出設計的確是有問題的。

綁定到成員變量

很多時候你需要 bind 數據成員而不是成員函數。例如,使用 std::map 或 std::multimap時,元素的類型是 std::pair<key const,data>, 但你想使用的信息通常不是 key, 而是 data. 假設你想把一個 map 中的每個元素傳遞給一個函數,它接受單個 data 類型的參數。你需要創建一個綁定器,它把每個元素(類型爲 std::pair)的 second 成員傳給綁定的函數。以下代碼舉例說明如何實現:

void print_string(const std::string& s) {
  std::cout << s << '\n';
}

std::map<int,std::string> my_map;
my_map[0]="Boost";
my_map[1]="Bind";

std::for_each(
  my_map.begin(),
  my_map.end(),
  boost::bind(&print_string, boost::bind(
    &std::map<int,std::string>::value_type::second,_1)));

你可以 bind 到一個成員變量,就象你可以綁定一個成員函數或普通函數一樣。要注意的是,要使得代碼更易讀(和寫),使用短的、方便的名字是個好主意。在前例中,對std::map 使用一個 typedef 有助於提高可讀性。

typedef std::map<int,std::string> map_type;
boost::bind(&map_type::value_type::second,_1)));

雖然需要 bind 到成員變量的時候沒有象成員函數那麼多,但是可以這樣做還是很方便的。SGI STL (及其派生的庫)的用戶可能很熟悉 select1st 和 select2nd 函數。它們用於選出 std::pair 的 first 或 second 成員,與我們在這個例子中所做的一樣。注意,bind可以用於任意類型和任意名字。

綁定還是不綁定

Boost.Bind 庫帶來了很大的靈活性,但是也給程序員帶來了挑戰,因爲有些時候本應該使用獨立的函數對象的,但也會讓人傾向於使用綁定器。許多工作可以也應該利用 Bind 來完成,但過度使用也是一種錯誤,應該在代碼開始變得難以閱讀、理解和維護的地方畫一條分界線。不幸的是,分界線的位置是由分享(閱讀、維護和擴展)代碼的程序員所決定的,他們的經驗決定了什麼是可以接受的,什麼不是。使用專門的函數對象的好處是,它們通常是無需加以說明的,而使用綁定器來提供同樣清楚的信息則是一項我們必須堅持克服的挑戰。例如,如果你需要創建一個你都很難弄明白的嵌套 bind ,有可能就是你已經過度使用了。讓我們用代碼來解釋一下。

#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <algorithm>
#include "boost/bind.hpp"

void print(std::ostream* os,int i) {
  (*os) << i << '\n';
}

int main() {
  std::map<std::string,std::vector<int> > m;
  m["Strange?"].push_back(1);
  m["Strange?"].push_back(2);
  m["Strange?"].push_back(3);
  m["Weird?"].push_back(4);
  m["Weird?"].push_back(5);

  std::for_each(m.begin(),m.end(),
    boost::bind(&print,&std::cout,
      boost::bind(&std::vector<int>::size,
        boost::bind(
          &std::map<std::string,
            std::vector<int> >::value_type::second,_1))));
}

上面這段代碼實際上做了什麼?有的人可以流暢地閱讀這段代碼[6],但對於我們多數人來說,需要一些時間才能搞清楚它是幹嘛的。是的,綁定器對 pair (即std::map<std::string,std::vector<int> >::value_type)的成員 second 調用成員函數size 。這種情況下,簡單的問題被綁定器弄得複雜了,創建一個小的函數對象來取代這個讓人難以理解的複雜綁定器是更好的選擇。一個可以完成相同工作的簡單函數對象如下:

[6] 你好,Peter Dimov.

class print_size {
  std::ostream& os_;
  typedef std::map<std::string,std::vector<int> > map_type;
public:
  print_size(std::ostream& os):os_(os) {}

  void operator()(
    const map_type::value_type& x) const {
    os_ << x.second.size() << '\n';
  }
};

這種時候使用函數對象的最大好處就是,名字是無需加以說明的。

std::for_each(m.begin(),m.end(),print_size(std::cout));

我們把這些(函數對象以及實際調用的所有代碼)和前面使用綁定器的版本作一下比較。

std::for_each(m.begin(),m.end(),
  boost::bind(&print,&std::cout,
    boost::bind(&std::vector<int>::size,
      boost::bind(
        &std::map<std::string,
          std::vector<int> >::value_type::second,_1))));

或者,如果我們負點責任,爲 vector 和 map 分別創建一個簡潔的 typedef :

std::for_each(m.begin(),m.end(),
  boost::bind(&print,&std::cout,
    boost::bind(&vec_type::size,
      boost::bind(&map_type::value_type::second,_1))));

這樣可以容易點分析,但它還是有點長。

雖然使用 bind 版本是有一些好理由,但我想觀點是很清楚的,綁定器不是非用不可的工具,使用時應該負責任,要讓它們物有所值。這一點在使用標準庫的容器和算法時非常、非常普遍。當事情變得太過複雜時,就回到老風格的方法上。

讓綁定器把握狀態

創建一個象 print_size 那樣的函數對象時,有幾個選項可用。我們在上一節中創建的那個版本中,保存了一個到 std::ostream 的引用,並使用這個 ostream 來打印map_type::value_type 參數的成員 second 的 size 函數的返回值。以下是原來的print_size :

class print_size {
  std::ostream& os_;
  typedef std::map<std::string,std::vector<int> > map_type;
public:
  print_size(std::ostream& os):os_(os) {}

  void operator()(
    const map_type::value_type& x) const {
    os_ << x.second.size() << '\n';
 }
};

要重點關注的一點是,這個類是有狀態的,狀態就在於那個保存的 std::ostream. 我們可以通過向調用操作符增加一個 ostream 參數來去掉這個狀態。這意味着這個函數對象將變爲無狀態的。

class print_size {
  typedef std::map<std::string,std::vector<int> > map_type;
public:
  typedef void result_type;
  result_type operator()(std::ostream& os,
    const map_type::value_type& x) const {
    os << x.second.size() << '\n';
  }
};

注意,這個版本的 print_size 可以很好地用於 bind, 因爲它增加了一個 result_type typedef. 這樣用戶在使用 bind 時就不需要顯式聲明函數對象的返回類型。在這個新版本的 print_size 裏,用戶需要傳遞一個 ostream 參數來調用它。這在使用綁定器時是很容易的。用這個新的 print_size 重寫前節中的例子,我們可以得到:

#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <algorithm>
#include "boost/bind.hpp"

// 省略 print_size 的定義

int main() {
  typedef std::map<std::string,std::vector<int> > map_type;
  map_type m;
  m["Strange?"].push_back(1);
  m["Strange?"].push_back(2);
  m["Strange?"].push_back(3);
  m["Weird?"].push_back(4);
  m["Weird?"].push_back(5);

  std::for_each(m.begin(),m.end(),
    boost::bind(print_size(),boost::ref(std::cout),_1));
}

細心的讀者可能覺得爲什麼 print_size 不是一個普通函數,畢竟它已經不帶有任何狀態了。事實上,它可以是普通函數。

void print_size(std::ostream& os,
  const std::map<std::string,std::vector<int> >::value_type& x) {
  os << x.second.size() << '\n';
}

還有更多的泛化工作可以做。我們當前版本的 print_size 要求其調用操作符的第二個參數是一個 const std::map<std::string,std::vector<int> > 引用,這不夠通用。我們可以做得更好一些,讓調用操作符對這個類型進行泛化。這樣,print_size 就可以使用任意類型的參數,只要該參數含有名爲 second 的公有成員,並且該成員有一個成員函數 size. 以下是改進後的版本:

class print_size {
public:
  typedef void result_type;
  template <typename Pair> result_type operator()
    (std::ostream& os,const Pair& x) const {
    os << x.second.size() << '\n';
  }
};

這個版本的用法與前一個是一樣的,但它更爲靈活。在創建可用於 bind 表達式的函數對象時,這種泛化更爲重要。因爲這樣的函數對象可用的情形將顯著增加,多數潛在的泛化都是值得做的。既然如此,我們還可以進一步放鬆對使用 print_size 的類型的要求。當前版本的 print_size 要求調用操作符的第二個參數是一個類似於 pair 的對象,即一個含有名爲 second 的成員的對象。如果我們決定只要求這個參數含有成員函數 size, 這個函數對象就真的與它的名字相符了。

class print_size {
public:
  typedef void result_type;
  template <typename T> void operator()
    (std::ostream& os,const T& x) const {
    os << x.size() << '\n';
  }
};

當然,儘管 print_size 現在是與它的名字相符了,但是我們也要求用戶要做的更多了。象對於我們前面的例子,就需要手工綁定一個 map_type::value_type::second.

std::for_each(m.begin(),m.end(),
  boost::bind(print_size(),boost::ref(std::cout),
    boost::bind(&map_type::value_type::second,_1)));

在使用 bind 時,通常都需要這樣的折衷,泛化只能到此爲止,不要損害到可用性。如果我們走到極端,甚至去掉對成員函數 size 的要求,那麼我們就轉了一圈,回到了我們開始的地方,又回到那個對多數程序員而言都過於複雜的 bind 表達式了。

std::for_each(m.begin(),m.end(),
  boost::bind(&print[7],&std::cout,
    boost::bind(&vec_type::size,
      boost::bind(&map_type::value_type::second,_1))));

[7] print 函數顯然也是需要的,沒有 Boost.Bind用法詳解(二) - qianw_ok - MagicianのBloglambda 工具。

關於 Boost.Bind 和 Boost.Function

雖然本章中討論的內容應該沒有遺漏了,但是對於 Boost.Bind 和另一個庫,Boost.Function,之間的配合還是值得一提,它可以提供更多的功能。我們將在 "Library 11:Function 11" 看到,不過我還是想給你一些提示。正如我們所看到的,沒有一個明顯的方法來保存我們的綁定器以備後用,我們只知道它們是帶有某些(未知)的特徵的兼容函數對象。但是,如果使用 Boost.Function, 保存函數用於以後的調用正是那個庫要做的,並且它兼容於 Boost.Bind, 可以把綁定器賦值給函數,保存它們並用於以後的調用。這是一個非常有用的概念,它可以用於適配並提高了松耦合。

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