直擊VS2010中的std::tr1 bind庫BUG
前兩天發(fā)現(xiàn)了VC2010 tr1庫中bind實(shí)現(xiàn)的一個(gè)bug,當(dāng)時(shí)只是作了記錄,沒有詳細(xì)分析.但作為一個(gè)QA,不找出問題所在實(shí)在不算稱職,于是就有了這篇捉蟲記.
閑言少敘,書歸正傳,tr1庫就不多作介紹了,有興趣的同學(xué)可以去 wikipedia上看.bind,顧名思義,就是把參數(shù)與函數(shù)綁定,以利于我們進(jìn)行函數(shù)式編程,是從boost的bind庫引入的,對(duì)bind不是很了解的可以看陳碩同學(xué)的這篇
以boost::function和boost:bind取代虛函數(shù)
假定筆者是 VC2010的QA,在做bind的功能測(cè)試,開始用gtest寫test case, (筆者一般用gtest作為C++測(cè)試框架,就寫到文章中了,但ms肯定不會(huì)用啦.筆者不在ms,請(qǐng)勿對(duì)號(hào)入座)
先寫幾個(gè)簡(jiǎn)單的函數(shù)供測(cè)試之用.
- int Add(int left, int right)
- {
- return left + right;
- }
- int Sub(int left, int right)
- {
- return left - right;
- }
- int Mul(int left, int right)
- {
- return left * right;
- }
再寫測(cè)試用例.先是幾個(gè)簡(jiǎn)單的
1 把值綁定到函數(shù)指針
- TEST(Bind, FundPtr_Values)
- {
- auto f0 = std::tr1::bind(Sub, 5, 3);
- ASSERT_EQ(2, f0());
- }
2 把值綁定到仿函數(shù)
- TEST(Bind, Functor_Values)
- {
- auto f0 = std::tr1::bind(std::minus<int>(), 6, 4);
- ASSERT_EQ(2, f0());
- }
3 把占位符綁定到函數(shù)指針
- TEST(Bind, FuncPtr_Placeholder)
- {
- auto f0 = std::tr1::bind(Sub, std::tr1::placeholders::_1, 8);
- ASSERT_EQ(12, f0(20));
- }
4 將占位符綁定到仿函數(shù)
- TEST(Bind, Functor_Placeholder)
- {
- auto f0 = std::tr1::bind(std::minus<int>(), std::tr1::placeholders::_2, std::placeholders::_1);
- ASSERT_EQ(12, f0(8, 20));
- }
運(yùn)行測(cè)試
- testing::InitGoogleTest(&argc, argv);
- int ret = RUN_ALL_TESTS();
一切OK.
來個(gè)復(fù)雜的,將bind返回結(jié)果再bind到函數(shù),用于進(jìn)行高階函數(shù)演算.
組合函數(shù)指針
- TEST(Bind, SquareDiff_FuncPtr)
- {
- auto f0 = std::tr1::bind(Mul,
- std::tr1::bind(Add, std::tr1::placeholders::_1, std::tr1::placeholders::_2),
- std::tr1::bind(Sub, std::tr1::placeholders::_1, std::tr1::placeholders::_2));
- ASSERT_EQ(16, f0(5, 3));
- }
運(yùn)行,一切OK,
再來一個(gè)仿函數(shù)版本
組合仿函數(shù)
- TEST(Bind, Squarediff_Functorptr)
- {
- auto f0 = std::tr1::bind(std::multiplies<int>(),
- std::tr1::bind(std::plus<int>(), std::tr1::placeholders::_1, std::tr1::placeholders::_2),
- std::tr1::bind(std::minus<int>(), std::tr1::placeholders::_1, std::tr1::placeholders::_2));
- ASSERT_EQ(16, f0(5, 3));
- }
編譯 , wow,這是啥啊
Error 1 error C2664: 'int std::multiplies<_Ty>::operator ()(const _Ty &,const _Ty &) const' : cannot convert parameter 1 from 'std::tr1::_Bind_fty<_Fty,_Ret,_BindN>' to 'const int &' c:\program files\microsoft visual studio 10.0\vc\include\xxcallobj 13 1 boostDemo
發(fā)現(xiàn)問題了, 偶兩眼開始放光.
現(xiàn)在就開bug么,不,太早了,總得指明問題在哪兒吧.是編譯器還是tr1庫實(shí)現(xiàn)的問題?怎么辦?拿另一個(gè)庫boost試試,正常
通過就是tr1的問題了
- TEST(Bind, SquareDiff_BoostFunctor)
- {
- auto f0 = boost::bind(std::multiplies<int>(),
- boost::bind(std::plus<int>(), _1, _2),
- boost::bind(std::minus<int>(),_1, _2));
- ASSERT_EQ(16, f0(5, 3));
- }
編譯運(yùn)行,一切正常.
現(xiàn)在可以肯定是tr1庫的問題么?還不能.也許對(duì)仿函數(shù)綁定是boost的擴(kuò)展而tr1準(zhǔn)標(biāo)準(zhǔn)并不支持呢?
打開標(biāo)準(zhǔn),看bind參數(shù)的說明 template<class F, class T1, class T2, ...., class TN>
unspecified bind(F f, T1 t1, T2 t2, ..., TN tN);
1 Requires: F and Ti shall be CopyConstructible. INVOKE (f, w1, w2, ..., wN) ([3.3]) shall be a valid expression
for some values w1, w2, ..., wN.
2 Returns: A forwarding call wrapper g with a weak result type ([3.3]). The effect of g(u1, u2, ..., uM) shall
be INVOKE (f, v1, v2, ..., vN, result_of<F cv (V1, V2, ..., VN)>::type), where cv represents
the cv-qualifiers of g and the values and types of the bound arguments v1, v2, ..., vN are determined as
specified below.
template<class R, class F, class T1, class T2, ...., class TN>
unspecified bind(F f, T1 t1, T2 t2, ..., TN tN);
3 Requires: F and Ti shall be CopyConstructible. INVOKE (f, w1, w2, ..., wN) shall be a valid expression for
some values w1, w2, ..., wN.
4 Returns: A forwarding call wrapper g with a nested type result_type defined as a synonym for R. The effect of
g(u1, u2, ..., uM) shall be INVOKE (f, v1, v2, ..., vN, R), where the values and types of the bound
arguments v1, v2, ..., vN are determined as specified below.
5 The values of the bound arguments v1, v2, ..., vN and their corresponding types V1, V2, ..., VN depend on
the type of the corresponding argument ti of type Ti in the call to bind and the cv-qualifiers cv of the call wrapper g as
follows:
— if ti is of type reference_wrapper<T> the argument is ti.get() and its type Vi is T&;
— if the value of std::tr1::is_bind_expression<Ti>::value is true the argument is ti(u1, u2, ...,
uM) and its type Vi is result_of<Ti cv (U1&, U2&, ..., UM&)>::type;
— if the value j of std::tr1::is_placeholder<Ti>::value is not zero the argument is uj and its type Vi is
Uj&;
— otherwise the value is ti and its type Vi is Ti cv &.
這里參數(shù)t,v,W來回出現(xiàn),挺繞的,書讀百遍,其義自現(xiàn) f為要綁定的函數(shù),其參數(shù)為 v1,v2,…,vN,類型為V1,V2,…,VN
bind 函數(shù)參數(shù)為t1, t2, …,tN,類型為T1,T2,…,TN
g 為bind的返回值,參數(shù)為參數(shù)為u1, u2,…uM,類型為U1, U2,…,UM.
g(u1, u2,…uM)會(huì)被轉(zhuǎn)發(fā)給f,轉(zhuǎn)發(fā)效果相當(dāng)于f(v1, v2, …,vN)
vi 如下確定
如果對(duì)應(yīng)bind函數(shù)參數(shù)ti類型為reference_wrapper<T>, 則vi為ti.get(),類型為T&
如果std::tr1::is_bind_expression<Ti>::value為true,即ti是bind的返回值,則vi為ti(u1,u2,…uM),類型為result_of<Ti cv(U1&,U2&,…,UM)>::type
如果std::tr1::is_placeholder<Ti>::value非0,則參數(shù)vi為uj,類型為 Uj&
否則vi值為ti,類型為Ti cv&
現(xiàn)在就很明了了,一個(gè)bind的返回值r1可以作為另一個(gè)bind的參數(shù)a1,在轉(zhuǎn)發(fā)時(shí)轉(zhuǎn)發(fā)的是r1(u1, u2,…,uM),即測(cè)試的代碼是符合標(biāo)準(zhǔn)的.那為什么通不過呢?再加上兩個(gè)測(cè)試,看看bind的返回值類型究竟能否讓 is_bind_expression<T>::value為true
is_bind_expression測(cè)試
- template<class T>
- bool IsBindResult(const T& v)
- {
- return std::tr1::is_bind_expression<T>::value;
- }
- TEST(IsBindResult, BindFuncPtr)
- {
- ASSERT_TRUE(IsBindResult(std::tr1::bind(Add, std::tr1::placeholders::_1, 2)));
- }
- TEST(IsBindResult, BindFunctor)
- {
- ASSERT_TRUE(IsBindResult(std::tr1::bind(std::plus<int>(), std::tr1::placeholders::_1, 2)));
- }
編譯,運(yùn)行
結(jié)果再次讓偶大躍眼鏡,居然bind的返回值沒能通過is_bind_expression的測(cè)試,怎么回事,打開is_bind_expression 的源碼來看
is_bind_expression定義
- template<class _Tx>
- struct is_bind_expression
- {
- static const bool value = false;
- };
- template<class _Result_type,
- class _Ret,
- class _BindN>
- struct is_bind_expression<
- _Bind<_Result_type, _Ret, _BindN> >
- {
- static const bool value = true;
- };
這里對(duì)模板作了一個(gè)特化,如果bind的模板參數(shù)是 _Bind<_Result_type, _Ret, _BindN>類型,則value為true,否則false.
看上去很美,沒問題,那為什么測(cè)試會(huì)失敗?難道bind(Add,…)返回的是_Bind類型而 bind(plus<int>(),…)返回的不是?接著看bind實(shí)現(xiàn):
函數(shù)指針版
bind到函數(shù)指針的返回類型
- template<class _Rx
- _C_CLASS_FARG0
- _C_CLASS_ARG0> inline
- _Bind<_Rx,
- _Rx,
- _BINDN<_Callable_fun<_Rx(__cdecl * const)(_FARG0_FARG1)> _C_ARG0_ARG1> >
- bind(_Rx(__cdecl * const _Val)(_FARG0_FARG1) _C_ARG0_A0)
- { // bind to pointer to function
- typedef _Callable_fun<_Rx(__cdecl * const)(_FARG0_FARG1)> _Callable;
- typedef _BINDN<_Callable _C_ARG0_ARG1> _MyBind;
- return (_Bind<_Rx, _Rx, _MyBind>(_MyBind(_Val _C_A0_A1)));
- }
返回的的確是_Bind類型,
仿函數(shù)版
bind到仿函數(shù)的返回類型
- template<class _Fty
- _C_CLASS_ARG0> inline
- _Bind_fty<_Fty,
- _Notforced,
- _BINDN<_Callable_obj<_Fty> _C_ARG0_ARG1> >
- bind(_Fty _Val _C_ARG0_A0)
- { // bind to UDT
- typedef _BINDN<_Callable_obj<_Fty> _C_ARG0_ARG1> _MyBind;
- return (_Bind_fty<_Fty, _Notforced, _MyBind>(_MyBind(_Val _C_A0_A1)));
- }
返回的卻是_Bind_fty類型, 難道_Bind_fty不是_Bind類型導(dǎo)致is_bind_expression返回false?接著看定義
_BindN和_Bind_fty的定義
- template<class _Ret,
- class _BindN>
- class _Bind<_Notforced, _Ret, _BindN>
- : public _Bind_base<_Ret, _BindN>
- {
- public:
- _Bind(_BindN _B0)
- : _Bind_base<_Ret, _BindN>(_B0)
- {
- }
- };
- template<class _Fty,
- class _Ret,
- class _BindN>
- class _Bind_fty
- : public _Wrap_result_type<(sizeof (::std:: tr1::_Has_result_type((_Fty *)0)) == sizeof (::std:: tr1::_Yes)), _Fty>,
- public _Bind_base<_Ret, _BindN>
- {
- public:
- _Bind_fty(_BindN _B0)
- : _Bind_base<_Ret, _BindN>(_B0)
- {
- }
- };
果然如此,真相大白.is_bind_expression不認(rèn)為 bind(functor…)是bind表達(dá)式,故會(huì)直接將其作為類型為_Bind_fty的參數(shù)轉(zhuǎn)發(fā)為 logical_and,導(dǎo)致編譯出錯(cuò).
找到錯(cuò)誤原因,可以開bug了 Title: std::tr1::bind can't bind a functor as an argument to another functor
How found: manual test
Build version:VC 1010 01019-532-2002102-70993
OS: Windows XP
Repro steps:
Run following code:
出錯(cuò)代碼
- TEST(Bind, Squarediff_Functorptr)
- {
- auto f0 = std::tr1::bind(std::multiplies<int>(),
- std::tr1::bind(std::plus<int>(), std::tr1::placeholders::_1, std::tr1::placeholders::_2),
- std::tr1::bind(std::minus<int>(), std::tr1::placeholders::_1, std::tr1::placeholders::_2));
- ASSERT_EQ(16, f0(5, 3));
- }
- Expected:
- Code got compiled and test passed. See n1836
- Result:
- Build break
- Note:
- The result of std::tr1::bind(std::minus<int>(), std::tr1::placeholders::_1, std::tr1::placeholders::_2)
- is of type _Bind_fty, which will make is_bind_expression<_Bind_fty>::value = false. Suggestion: modify the
- return type of bind or change is_bind_expression to make is_bind_expression<_Bind_fty>::value to be true.
提交bug, Ok.
結(jié)束了么?還沒有,需要編寫一個(gè)測(cè)試來驗(yàn)證這個(gè)問題并加到測(cè)試列表里去,可是直接寫會(huì)導(dǎo)致編譯出錯(cuò),怎么辦呢?
有辦法,
將如下代碼保存到一個(gè)文件去
測(cè)試代碼
- #include <iostream>
- #include <conio.h>
- #include <tchar.h>
- int main()
- {
- auto f0 = std::tr1::bind(std::multiplies<int>(),
- std::tr1::bind(std::plus<int>(), std::tr1::placeholders::_1, std::tr1::placeholders::_2),
- std::tr1::bind(std::minus<int>(), std::tr1::placeholders::_1, std::tr1::placeholders::_2));
- return f0(5, 3);
- }
調(diào)用cl編譯這個(gè)文件,如果能得到一個(gè)可執(zhí)行程序并且執(zhí)行后返回16則測(cè)試通過,否則測(cè)試失敗.
代碼就不在這里列舉了.
寫完測(cè)試,添加到daily build的test list中去,什么時(shí)候問題解決了從測(cè)試結(jié)果上就能看出來了.
注
1 文中寫的找bug的過程是直線式的邏輯,思路很清晰,實(shí)際我在跟代碼的時(shí)候就像在一個(gè)迷宮里打轉(zhuǎn),只看見出了錯(cuò)誤,不知道什么地方錯(cuò)了,只好用各種手段, 想先找出bind在綁定函數(shù)指針時(shí)究竟干了什么,再來推理為什么綁定仿函數(shù)會(huì)出錯(cuò),根本不是象文中寫的那 樣直接推斷到is_bind_expression的問題.
VC2010里tr1庫的實(shí)現(xiàn)實(shí)在不好閱讀,滿篇全是宏,連函數(shù)調(diào)用傳了幾個(gè)參數(shù)都搞不清楚,我只好先用/E選項(xiàng)把預(yù)編譯的文件輸出出來,在跟蹤代碼時(shí)一邊看預(yù)處理后的代碼一邊看匯編,無論哪個(gè)都比tr1自己的源代碼好讀.
不過這一番跟蹤下來收獲倒也頗豐,
1) 基本清楚了bind的實(shí)現(xiàn),可以另外寫篇博客來談
2) 明白了RVO(返回值優(yōu)化)的原理.在跟蹤bind匯編代碼時(shí)時(shí)發(fā)現(xiàn)只要三個(gè)參數(shù),卻push了四個(gè),調(diào)用完成后也是ret 10返回,跟了下去才發(fā)現(xiàn)是做的RVO
2 文中用到了ASSERT宏進(jìn)行測(cè)試,也算是對(duì)前些天對(duì)我這篇金山衛(wèi)士代碼批評(píng)評(píng)論里大家對(duì)ASSERT質(zhì)疑的一個(gè)回應(yīng)吧.c++庫里的assert的作用是及時(shí)發(fā)現(xiàn)錯(cuò)誤反饋給程序員, 會(huì)打斷程序執(zhí)行,而測(cè)試框架里的ASSERT則是斷言測(cè)試是否符合預(yù)期,把結(jié)果傳遞給測(cè)試框架,再由測(cè)試框架記錄后反饋給程序員.我原來以為在那個(gè)上下文都是測(cè)試情況下assert的語義應(yīng)當(dāng)不言自明的是指后者,沒想到還是有很多讀者誤以為是c++庫里的assert.這是我沒有把話說清楚,假定自己知道的受眾也知道,忽略了背景的區(qū)別帶來的對(duì)同一個(gè)名詞不同的理解,是我經(jīng)常犯的一個(gè)錯(cuò)誤,要努力改正.
3 QA除了寫測(cè)試用例,用工具和腳本進(jìn)行測(cè)試外還可以在項(xiàng)目過程中參與更多.一個(gè)好的QA應(yīng)當(dāng)有不弱于Dev的編碼能力, 有能力復(fù)查Dev設(shè)計(jì)和編碼,直接從中發(fā)現(xiàn)問題,以及在測(cè)試中發(fā)現(xiàn)問題時(shí)有能力定位bug源頭并給出參考解決方案.QA還要有縝密的思維和想象能力,對(duì)邊界條件、各種邏輯組合和極端情況能去構(gòu)造和評(píng)估其對(duì)功能的影響,因?yàn)镈ev一般習(xí)慣于正常情況下的邏輯,邊界情況雖然也會(huì)考慮,但還是不會(huì)有QA想的全面.遺憾的是目前國內(nèi)開發(fā)團(tuán)隊(duì)中這樣的QA還是比較少見.
原文鏈接:http://www.cnblogs.com/MichaelPeng/archive/2010/12/27/ABugReportOnVC2010_std_tr1_bind.html
【編輯推薦】
- Visual Studio自定義調(diào)整窗體的兩個(gè)小技巧
- Visual Studio 2010中關(guān)于C#的幾點(diǎn)改進(jìn)
- Visual Studio 2010及.Net 4新功能一覽
- 提高效率 用好Visual Studio 2010自定義代碼段