1、bind的工作原理
bind 並不是一個單獨的類或函數,而是非常龐大的家族,依據綁定的參數的個數和要綁定的調用對象的類型,總共有數十種不同的形式,編譯器會根據具體的綁定代碼制動確定要使用的正確的形式,bind的基本形式如下:
template<class R,class F> bind(F f);
template<class R,class F,class A1> bind(F f,A1 a1);
namespace
{
boost::arg<1> _1;
boost::arg<2> _2;
boost::arg<3> _3;
….. //其他6個占位符
};
bind 接收的第一個參數必須是一個可調用的對象f,包括函數、函數指針、函數對象、和成員函數指針,之后bind最多接受9個參數,參數數量必須與f的參數數量相等,這些參數被傳遞給f作為入參。 綁定完成后,bind會返回一個函數對象,它內部保存了f的拷貝,具有operator(),返回值類型被自動推導為f的返回類型。在發生調用時這個函數對象將把之前存儲的參數轉發給f完成調用。例如,有一個函數func,它的形式是:
func(a1,a2);
那么,他將等價於一個具有無參operator()的bind函數對象調用:
bind(func,a1,a2)();
這是bind最簡單的形式,bind表達式存儲了func和a1、a2的拷貝,產生了一個臨時函數對象。因為func接收兩個參數,而a1和a2的拷貝傳遞給func完成真正的函數調用。
bind的真正威力在於它的占位符,它們分別定義為_1,_2,_3,一直到 _9,位於一個匿名的名字空間。占位符可以取代bind參數的位置,在發生調用時才接受真正的參數。占位符的名字表示它在調用式中的順序,而在綁定的表達式中沒有沒有順序的要求,_1不一定必須第一個出現,也不一定只出現一次,例如:
bind(func,_2,_1)(a1,a2);
返回一個具有兩個參數的函數對象,第一個參數將放在func的第二個位置,而第二個參數則放在第一個位置,調用時等價於:
func(a2,a1);
2、綁定普通函數
bind可以綁定普通函數,包括函數、函數指針,假設我么有如下的函數定義:
int f(int a,int b){return a+b;} //二元函數
int g(int a,int b,int c) {return a+b+c;} //三元函數
typedef int (*f_type)(int,int); //函數指針定義
typedef int (*g_type)(int,int,int); //函數指針定義
那么,bind(f,1,2) 將返回一個無參調用函數對象,等價於f(1,2),bind(q,1,2,3)同樣返回一個無參調用的函數對象,等價於 g(1,2,3)。這兩個綁定表達式沒有使用占位符,而是給出了全部的具體參數,代碼:
cout<<bind(f,1,2)()<<endl;
cout<<bind(g,1,2,3)()<<endl;
相當於:
cout<<f(1,2)<<endl;
cout<<g(1,2,3)<<endl;
使用占位符bind可以有更多的變化,這才是它真正應該做的工作,下面列出了一些占位符的用法:
bind(f,_1,9)(x); //f(x,9),相當於bind2nd(f,9)
bind(f,_1,_2)(x,y); //f(x,y)
bind(f,_2,_1)(x,y); //f(y,x)
bind(f,_1,_1)(x,y); //f(x,x),y參數被忽略
bind(g,_1,8,_2)(x,y) //g(x,8,y)
bind(g,_3,_2_2)(x,y,z) //g(z,y,y),x參數被忽略
注意:必須在綁定表達式中提供函數要求的所有參數,無論是真實參數還是占位符均可以。占位符可以出現也可以不出現,出現的順序和數量沒有限定,但不能使用超過函數參數數量的占位符,比如在綁定f是不能用_3,在綁定g時不能使用_4,也不能寫bind(f,_1,_2,_2),這樣的形式會導致編譯錯誤。bind完全可以代替標准庫中的bind1st和bind2nd,使用bind(f,N,_1)和bind(f,_1,N)。要注意的是它們均使用了一個占位符,bind1st把第一個參數用固定值代替,bind2nd把第二個參數用固定值代替。bind也可以綁定函數指針,用法相同,例如:
f_type pf = f;
g_type pg = g;
int x =1,y=2,z=3;
cout<<bind(pf,_1,9)(x)<<endl; //(*pf(x,9))
cout<<bind(pg,_3,_2,_2)(x,y,z)<<endl; //(*pg)(z,y,y)
3、bind綁定成員函數
類的成員函數不同於普通的函數,因為成員函數指針不能直接調用operator(),它必須被綁定到一個對象或指針,然后才能得到this指針進而調用成員函數。因此bind需要 “犧牲”一個占位符,要求提供一個類的實例、引用或者指針,通過對象作為第一個參數來調用成員函數,即:
bind(&X::func,x,_1,_2,…)
這意味着使用成員函數時只能最多綁定8個參數。例如,有一個類demo
struct demo
{
int f(int a,int b){return a+b;}
};
那么,下面的bind表達式都是成立的:
demo a,&ra = a; //類的實例對象和引用
demo * p = & a; //指針
cout<<bind(&demo::f,a,_1,20)(10)<<endl;
cout<<bind(&demo::f,ra,_2,_1)(10,20)<<endl;
cout<<bind(&demo::f,p,_1,_2)(10,20)<<endl;
注意:我們必須在成員函數前面加上取地址的操作符&,表明這是一個成員函數指針,否則會無法編譯通過,這是與綁定函數的一個小小的不同。bind同樣支持綁定虛擬成員函數,用法與非虛函數相同,虛函數的行為將由實際調用發生時的實例來決定。
4、bind綁定成員變量
bind的另一個對類的操作是它可以綁定public成員變量,用法與綁定成員函數類似,只需要把成員變量名像一個成員函數一樣去使用。例如:
vector<point> v(10);
vector<int> v2(10);
transform(v.begin(),v.end(),v2.begin(),bind(&point::x,_1));
BOOST_FOREACH(int x,v2) cout<<x<<“,”;
代碼中的bind(&point::x,_1)取出point對象的成員變量x,transform算法調用bind表達式操作容器v,逐個把成員變量填入到v2中。看到這里感到有點困惑,有點難以理解:bind返回的是一個函數對象,該對象對“()”進行了重載,在transform調用該重載函數應該是將v中的每一個成員變量作為參數傳進去,從而取出每一個元素的x變量。
使用bind可以實現SGISTL/STLport中的非標准函數適配器select1st和select2nd的功能,直接選擇出pair對象first和second成員,例如:
typedef pair<int,string> pair_t;
pair_t p(123,”string”);
cout<<bind(&pair_t::first,p)()<<endl;
cout<<bind(&pair_t::second,p)()<<endl;
5、綁定函數對象
bind不僅能夠綁定函數和函數指針,也能夠綁定任意的函數對象,包括標准庫中預定義的函數對象。如果函數對象有內部類型定義result_type,那么bind可以自動推導出返回值類型,用法與普通函數一樣。但如果函數對象沒有定義result_type,則需要在綁定形式上做一點改動,用模板參數指明返回類型,像這樣:
bind<result_type>(Functor,…);
標准庫和Boost庫中的大部分函數都具有result_type定義,因此不需要特別的形式就可以直接使用bind,例如:
bind(std::greater<int>(),_1,10); //檢查 x>10
bind(plus<int>(),_1,_2); //執行 x+y
bind(modulus<int>(),_1,3), //執行 x%3
對於自定義的函數對象,如果沒有result_type類型定義,例如:
struct f
{
int operator() (int a,int b)
{return a +b;}
};
那么我們必須指明
bind<int> (f(),_1,_2)(10,20)<<endl;
這種寫法所燒會有些不方便,因此,在編寫自己的函數對象時,最好遵循規范為它們增加內部typedef result_type,這將使函數對象與其他的標准庫和Boost庫組件配合工作。
6、使用ref庫
bind采用拷貝的方式保存綁定對象和參數,這意味着綁定表達式中的每一個變量都會有一份拷貝,如果函數對象或值參數很大、拷貝代價很高,或者無法拷貝,那么bind的使用就會受到限制。因此bind庫可以搭配ref庫使用,ref庫包裝了對象的引用,可以讓bind存儲對象引用的拷貝,從而降低了拷貝的代價。但這也帶來了一個隱患,因為有時候bind的調用可能會延后很久,程序員必須保證bind被調用時引用是有效的。如果調用是引用的變量或者函數對象你被銷毀了,那么將會發生未定義行為。ref配合bind用法的代碼如下:
int x = 10;
cout<<bind(g,_1,cref(x),ref(x))(10)<<endl;
f af;
cout<<bind<int>(ref(af),_1,_2)(10,20)<<endl;
下面的代碼則因為引用失效,引發未定義行為:
int x = 10;
BOOST_AUTO(r,ref(x));
{
int * y = new int(20);
r = ref(*y);
cout<<r<<endl;
cout<<bind(g,r,1,1)()<<endl;
delete y;
}
cout<<bind(g,r,1,1)()<<endl;
7、存儲bind表達式
很多時候我們需要把寫好的bind表達式存儲起來,以便稍后再度使用,但bind表達式生成的對象類型聲明非常復雜,通常無法寫出正確的類型,因此可以使用typeof庫的BOOST_AUTO宏來輔助我們,例如:
BOOST_AUTO(x,bind(greater<int>(),_1,_2));
cout<<x(10,20)<<endl;
bind 可以嵌套,一個bind表達式生成的函數對象可以被另一個bind在綁定,從而實現類似f(g(x))的形式,如果我們有f(x)和g(x)兩個函數,那么f(g(x))的bind表達式就是:
bind(f,bind(g,_1))(x)
使用bind的嵌套用法必須小心,它不太容易一次寫對,也不太容易理解,超過兩個以上的bind表達式通常只能被編譯器讀懂,必須配合良好的注釋才能夠使用bind嵌套用法。
8、綁定非標准函數
bind可以適配任何一種C++中的函數,但標准形式bind(f,…)不是適用所用的情況,有些非標准函數無法制動推導出返回值類型,典型的就是C中的可變參數函數printf()。必須用bind<int>(printf,…)(…),例如:
bind<int>(printf,”%d+%d=%d\n”,_1,_1,_2)(6,7);
bind的標准形式也不能支持使用了不同的調用方式,如:__stdcall、__fastcall、extern”C” 的函數,通常bind把他們看做函數對象,需要顯示的指定bind的返回值類型才能綁定。