前两天发现了VC2010 tr1库中bind实现的一个bug,当时只是作了记录,没有详细分析.但作为一个QA,不找出问题所在实在不算称职,于是就有了这篇捉虫记.
闲言少叙,书归正传,tr1库就不多作介绍了,有兴趣的同学可以去 wikipedia上看.bind,顾名思义,就是把参数与函数绑定,以利于我们进行函数式编程,是从boost的bind库引入的,对bind不是很了解的可以看陈硕同学的这篇
以boost::function和boost:bind取代虚函数
假定笔者是 vc2010的QA,在做bind的功能测试,开始用gtest写test case, (笔者一般用gtest作为C++测试框架,就写到文章中了,但ms肯定不会用啦.笔者不在ms,请勿对号入座)
先写几个简单的函数供测试之用.
{
return left + right;
}
int Sub( int left, int right)
{
return left - right;
}
int Mul( int left, int right)
{
return left * right;
}
再写测试用例.先是几个简单的
1 把值绑定到函数指针
{
auto f0 = std::tr1::bind(Sub, 5 , 3 );
ASSERT_EQ( 2 , f0());
}
2 把值绑定到仿函数
{
auto f0 = std::tr1::bind(std::minus < int > (), 6 , 4 );
ASSERT_EQ( 2 , f0());
}
3 把占位符绑定到函数指针
{
auto f0 = std::tr1::bind(Sub, std::tr1::placeholders::_1, 8 );
ASSERT_EQ( 12 , f0( 20 ));
}
4 将占位符绑定到仿函数
{
auto f0 = std::tr1::bind(std::minus < int > (), std::tr1::placeholders::_2, std::placeholders::_1);
ASSERT_EQ( 12 , f0( 8 , 20 ));
}
运行测试
int ret = RUN_ALL_TESTS();
一切OK.
来个复杂的,将bind返回结果再bind到函数,用于进行高阶函数演算.
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
{
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 ));
}
运行,一切OK,
再来一个仿函数版本
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
{
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 |
发现问题了, 偶两眼开始放光.
现在就开bug么,不,太早了,总得指明问题在哪儿吧.是编译器还是tr1库实现的问题?怎么办?拿另一个库boost试试,正常
通过就是tr1的问题了
{
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 ));
}
编译运行,一切正常.
现在可以肯定是tr1库的问题么?还不能.也许对仿函数绑定是boost的扩展而tr1准标准并不支持呢?
打开标准,看bind参数的说明
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 &.
这里参数t,v,W来回出现,挺绕的,书读百遍,其义自现
f为要绑定的函数,其参数为 v1,v2,…,vN,类型为V1,V2,…,VN
bind 函数参数为t1, t2, …,tN,类型为T1,T2,…,TN
g 为bind的返回值,参数为参数为u1, u2,…uM,类型为U1, U2,…,UM.
g(u1, u2,…uM)会被转发给f,转发效果相当于f(v1, v2, …,vN)
vi 如下确定
- 如果对应bind函数参数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,则参数vi为uj,类型为 Uj&
- 否则vi值为ti,类型为Ti cv&
现在就很明了了,一个bind的返回值r1可以作为另一个bind的参数a1,在转发时转发的是r1(u1, u2,…,uM),即测试的代码是符合标准的.那为什么通不过呢?再加上两个测试,看看bind的返回值类型究竟能否让 is_bind_expression<T>::value为true
is_bind_expression测试
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
)));
}
编译,运行
结果再次让偶大跃眼镜,居然bind的返回值没能通过is_bind_expression的测试,怎么回事,打开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
;
};
这里对模板作了一个特化,如果bind的模板参数是 _Bind<_Result_type, _Ret, _BindN>类型,则value为true,否则false.
看上去很美,没问题,那为什么测试会失败?难道bind(Add,…)返回的是_Bind类型而 bind(plus<int>(),…)返回的不是?接着看bind实现:
函数指针版
bind到函数指针的返回类型
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类型,
仿函数版
bind到仿函数的返回类型
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类型导致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不认为 bind(functor…)是bind表达式,故会直接将其作为类型为_Bind_fty的参数转发为 logical_and,导致编译出错.
找到错误原因,可以开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:
出错代码
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.
结束了么?还没有,需要编写一个测试来验证这个问题并加到测试列表里去,可是直接写会导致编译出错,怎么办呢?
有办法,
将如下代码保存到一个文件去
测试代码
#include
<
iostream
>
#include
<
conio.h
>
#include
<
tchar.h
>
int
main()
{
auto f0
=
std::tr1::bind(std::multiplies
&
lt;
int
&
gt;(),
std::tr1::bind(std::plus
&
lt;
int
&
gt;(), std::tr1::placeholders::_1, std::tr1::placeholders::_2),
std::tr1::bind(std::minus
&
lt;
int
&
gt;(), std::tr1::placeholders::_1, std::tr1::placeholders::_2));
return
f0(
5
,
3
);
}
调用cl编译这个文件,如果能得到一个可执行程序并且执行后返回16则测试通过,否则测试失败.
代码就不在这里列举了.
写完测试,添加到daily build的test list中去,什么时候问题解决了从测试结果上就能看出来了.
--------------------------正文的分割线 -------------------------------
注
1 文中写的找bug的过程是直线式的逻辑,思路很清晰,实际我在跟代码的时候就像在一个迷宫里打转,只看见出了错误,不知道什么地方错了,只好用各种手段, 想先找出bind在绑定函数指针时究竟干了什么,再来推理为什么绑定仿函数会出错,根本不是象文中写的那 样直接推断到is_bind_expression的问题.
vc2010里tr1库的实现实在不好阅读,满篇全是宏,连函数调用传了几个参数都搞不清楚,我只好先用/E选项把预编译的文件输出出来,在跟踪代码时一边看预处理后的代码一边看汇编,无论哪个都比tr1自己的源代码好读.
不过这一番跟踪下来收获倒也颇丰,
1) 基本清楚了bind的实现,可以另外写篇博客来谈
2) 明白了RVO(返回值优化)的原理.在跟踪bind汇编代码时时发现只要三个参数,却push了四个,调用完成后也是ret 10返回,跟了下去才发现是做的RVO
2 文中用到了ASSERT宏进行测试,也算是对前些天对我这篇金山卫士代码批评评论里大家对ASSERT质疑的一个回应吧.c++库里的assert的作用是及时发现错误反馈给程序员, 会打断程序执行,而测试框架里的ASSERT则是断言测试是否符合预期,把结果传递给测试框架,再由测试框架记录后反馈给程序员.我原来以为在那个上下文都是测试情况下assert的语义应当不言自明的是指后者,没想到还是有很多读者误以为是c++库里的assert.这是我没有把话说清楚,假定自己知道的受众也知道,忽略了背景的区别带来的对同一个名词不同的理解,是我经常犯的一个错误,要努力改正.
3 QA除了写测试用例,用工具和脚本进行测试外还可以在项目过程中参与更多.一个好的QA应当有不弱于Dev的编码能力, 有能力复查Dev设计和编码,直接从中发现问题,以及在测试中发现问题时有能力定位bug源头并给出参考解决方案.QA还要有缜密的思维和想象能力,对边界条件、各种逻辑组合和极端情况能去构造和评估其对功能的影响,因为Dev一般习惯于正常情况下的逻辑,边界情况虽然也会考虑,但还是不会有QA想的全面.遗憾的是目前国内开发团队中这样的QA还是比较少见.
参考资料