深圳夜归人

繁华的都市,有谁记得我们的脚步?

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  32 随笔 :: 0 文章 :: 63 评论 :: 1 引用

2005-07-28 15:49  重写了所有代码,不再把对象指针和函数指针转换为void*,而是保留原类型,通过虚函数让编译器来完成绑定。新版本下载

2005-07-27 10:36  返回值为void的特化版本改为直接循环,其它返回值的处理方式不变,依旧是Ret r = call(...); for (;;;) ret = call (...);这种方式。以后有好的方案再修改。

2005-07-26 21:36  修正G++编译的版本段错误(感谢 问题男),经初步测试没有发现问题。

2005-07-26 09:51  修正call函数中缺少typename的错误(感谢
bcpl ),增加了Makefile(需要系统中安装有python),段错误的大问题还没有解决(确认VC8中不存在这问题)。

2005-07-25 23:55  修正代码生成器脚本中的2处错误,修正处理返回值的失误,增加测试代码。


相比前一版本,修改了以下几点(下标从0开始:))
0、类名改为Delegate。
1、采用Delegate < int& (const stirng&, float) > d这样的语法,更直观(学boost::function的)。
2、可以绑定仿函数了。
3、支持最多26个参数。。。(使用python脚本生成的,用宏太累)
4、如果返回值不是void,那么当委托没有绑定任何对象时,调用将抛出DelegateNotHandled异常。
5、支持=、+=、()操作符。
6、支持绑定委托对象。因为委托对象本身也是个仿函数,所以这里把它按仿函数来处理了。



重要提示:
0、Delegate.h文件是由generator.py读取其它3个文件,并替换其中定义的格式化字符串来生成的。
1、不要用它来绑定多继承类的成员函数。

扩充话题:
和boost::function比较,本文的Delegate类型要求比较严格,我认为这是好事。boost::function可以定义一个int(int)类型的function,但绑定一个short(double)类型的函数,实际并不是好事,我认为。

Delegate解决这类问题的方法是使用Adapter,这里给一个例子:

short funcB (double d)
{
    
return (short)d;
}

struct FuncA2FuncBAdapter
{
    Delegate 
<short(double)> adapter;
    
int operator ( ) (int n)
    {
        
return (int)adapter ((double)n);
    }
};

int main ()
{
    Delegate 
<int(int)> d_test;
    FuncA2FuncBAdapter adp;
    d_test 
+= adp;
    adp.adapter 
+= funcB;
    d_test (
3);
    
return 0;
}

这样可以明确地把强制转型的责任交给程序员,而不是编译器偷偷的做。

用Adapter还可以做更复杂的,下面这个boost::function就不能直接转了:

pair<shortshort> funcB (double a, int b)
{
    
return make_pair((short)a, (short)b);
}

struct FuncA2FuncBAdapter
{
    Delegate 
<pair<shortshort>(doubleint)> adapter;
    
int operator ( ) (int n)
    {
        
return (int)adapter ((double)n, n+5).first;
    }
};

int main ()
{
    Delegate 
<int(int)> d_test;
    FuncA2FuncBAdapter adp;
    d_test 
+= adp;
    adp.adapter 
+= funcB;
    d_test (
3);
    
return 0;
}

另一个差别是本文的Delegate类是多分派的。

下载:
http://files.cnblogs.com/cpunion/Delegate.rar

感谢:
  问题男 bcpl 刘未鹏(blog中关于函数类型的讲解)。


刚在comp.lang.c++@googlegroups.com上看到的一个CallBack类,摘录在此。

// file sltest.h
#ifndef SLTEST_H
#define SLTEST_H

class CallbackBase
{
public:
    
virtual void operator()() const { };
    
virtual ~CallbackBase() = 0;
};

CallbackBase::
~CallbackBase() { }
template
<typename T>
class Callback : public CallbackBase
{
public:
    typedef 
void (T::*F)();
    Callback( T
& t, F f ) : t_(&t), f_(f) { }
    
void operator()() const { (t_->*f_)(); }
private:
    T
* t_;
    F  f_;
};

template
<typename T>
Callback
<T> make_callback( T& t, void (T::*f) () )
{
    
return Callback<T>( t, f );
}
posted on 2005-07-25 17:51 cpunion 阅读(1186) 评论(32)  编辑 收藏 网摘 所属分类: 1. c/c++

评论

#1楼  2005-07-26 02:04 bcpl      
在mingw下需要在call函数中加typename,否则编译不过,不过即使改好编译通过,运行也会出现Segmentation fault。。
  回复  引用  查看    

#2楼 [楼主] 2005-07-26 03:14 cpunion [未注册用户]
的确。。。。的确。。

偶然醒来,看了你的回复,测试了一下,的确是这样。typename问题我已经处理了,明天有空再看看段错误是怎么回事。。

这个问题存在于GCC下,其它编译器也可能存在这个问题,目前只测试了VC 2005。
  回复  引用    

#3楼  2005-07-26 09:48 cpunion [未注册用户]
还没有解决。已经确认,在call中,obj和func这2个指针的值都是正确的,只是
typename FunctionTraits<NullType, Ret>::mem_fun0 mf;
*(void**)&mf = func;
return (((NullType*)obj)->*mf)();
最后调用时,在类成员函数中获取的this指针是错误的。

一样的代码写在main函数中测试则没有这问题,在VC8中测试没有发现问题。

不知道g++中如何嵌入汇编呢?
  回复  引用    

#4楼 [楼主] 2005-07-26 10:10 cpunion      
其它未解决的问题:
1、委托对象拷贝构造、赋值的问题。由于它管理了仿函数对象副本的生存期,析构或调用=操作符时,会析构现有的仿函数对象,如果复制委托对象,会多次析构,这个暂时没有处理。

  回复  引用  查看    

#5楼 [楼主] 2005-07-26 14:21 cpunion      
已经完成了上面提到的拷贝构造函数、赋值操作符。
  回复  引用  查看    

#6楼  2005-07-26 17:06 问题男 [未注册用户]
楼主您还得仔细查查代码,其中必有内存管理不善的部分,我没有细看,尚不知道问题出在哪里,但基本的调用方法还是可行的

请看,当demo的main函数中代码被缩减为:
bound_method_test::Test test;
int (bound_method_test::Test::*fp)() = &bound_method_test::Test::test0_return_int;
int (DummyType::*fp1)();
*(void**)&fp1 = *(void**)&fp;
cout << (((DummyType*)&test)->*fp1)() << endl;
且注释掉调用其他代码时,运行正常,可见调用机制还是没有问题的(我也想不出有什么理由有问题,呵呵),然后逐步把注释的代码还原,直到有任何一句使用Delegate<...>::operator+=后,运行异常,可见有内存的误操作在其中

测试环境为g++ 3.4.2(mingw)

  回复  引用    

#7楼 [楼主] 2005-07-26 17:20 cpunion      
在成员函数中输出this地址,可以看出来和call中的obj指针值不同,而call中这个值是正确的,还不清楚具体的原因。
  回复  引用  查看    

#8楼  2005-07-26 17:35 问题男 [未注册用户]
初步估计是内存使用的问题,越界访问能令任何值呈现未知的状态,包括堆和栈中的,重点检查DelegateBase::_managed_objects对象的行为不失为有效的举措
  回复  引用    

#9楼  2005-07-26 20:05 问题男 [未注册用户]
一直从目标码角度分析,见:
(((NullType *)obj)->*mf)();
这句被翻译成
004124A9 |> 8B45 FC mov eax, [ebp-4]
004124AC |. 0345 0C add eax, [ebp+C]
004124AF |. 890424 mov [esp], eax
004124B2 |. FF55 F0 call [ebp-10]
实在令人费解,其中[ebp+c]中的值乃是obj的值,但[ebp-4]这个为何我就不清楚了,上下文根本就没有使用这个变量,为什么要加到this指针上还真是奇怪,且其值总不为零,导致this指针错误,但在其他地方却不是这么翻译的,真是怪哉~~~~

随后,更是得到一个莫名其妙解决方法,将
typename FunctionTraits<NullType, Ret>::mem_fun0 mf;
替换成
typename FunctionTraits<NullType, Ret>::mem_fun0 mf = NULL;
居然全都正常了,原因尚不明,在正常时,上述[ebp-4]值居然为零了

另外,我估计错误,没有迹象表明有内存操作错误,呵呵

  回复  引用    

#10楼  2005-07-26 20:06 问题男 [未注册用户]
btw:从反向工程的结果看,g++默认的“thiscall”方式有点像__stdcall,是把this当作第一个参数压栈的
  回复  引用    

#11楼 [楼主] 2005-07-26 21:36 cpunion [未注册用户]
果然非常奇怪,居然是这样一个问题,哎。。。真是太感谢了,打死我也想不到这样来解决。。。

g++的确是把this作为第一个参数压栈,以前我使用类似的方法,把它的this参数从第一个参数传递,结果调用是成功的。《深入探索C++对象模型》一书讲过,第一个C++编译器是这样来实现的,看来G++也这样干的。

按你说的办法修改了,在G++和VC下都测试通过。重新上传。
  回复  引用    

#12楼 [楼主] 2005-07-26 21:41 cpunion [未注册用户]
怀疑是不是被G++给优化成这样的。。

另外一个需要优化的地方,是在call中定义了一个成员函数指针,理论上讲应该是可以省掉的,不过G++下不行,有空再看看。。

目前的版本速度不是十分满意,非虚函数调用效率只有直接调用的1/10,应该不是vector遍历的影响吧?也可能与编译器优化有关,下次测试一下虚函数调用效率。
  回复  引用    

#13楼  2005-07-26 22:06 问题男 [未注册用户]
我认为可以不必在乎效率,如果一定要优化,可以这么考虑看看

1、不要单独写一个call了,直接在operator()完成就好

2、还是那个观点,操作vector,可以尝试用iterator替代operator[]
vector <pair<void*, void*> >::const_iterator itr = _handlers.begin();
vector <pair<void*, void*> >::const_iterator itrEnd = _handlers.end();

for(; itr != itrEnd; ++itr)
{
if(!itr->first)
{
......
}
else
{
......
}
}

还有,没看出为什么要第一个元素要单独call,然后后面的再一起call
  回复  引用    

#14楼 [楼主] 2005-07-26 23:08 cpunion [未注册用户]
哦。单独call是因为你定义一个Ret ret;不给它赋值,编译器会警告,就为了这个,呵呵。。有办法避免这个警告吗?其实也是为了它才增加了call函数。
  回复  引用    

#15楼  2005-07-26 23:29 问题男 [未注册用户]
警告可以不理他,呵呵,也可以试试
Ret r;
r = Ret();
  回复  引用    

#16楼 [楼主] 2005-07-26 23:44 cpunion [未注册用户]
哦。无返回值的版本比较好处理,不过有返回值的版本,如果返回值是个引用,则无法这样来做,这也是要单独提出来的原因。

不知道有没有好的做法。
  回复  引用    

#17楼 [楼主] 2005-07-26 23:54 cpunion [未注册用户]
看来需要一个Traits:
Ret ret = TrueTypeTraits<Ret>::type ();
用TrueTypeTraits来提取真正的类型。

不过这会有另一个问题,如果返回值是引用,而这个类型本身不支持默认构造。。。。

呵呵,看来需要把返回值是引用的单独处理。
  回复  引用    

#18楼  2005-07-26 23:58 问题男 [未注册用户]
引用的话
int &r = call(...);
for(;;)
{
r = call(...);
}
这样仍旧是有问题的阿,呵呵,而且
Ret r;
r = Ret();
在引用的情况下亦可通过

其实这里保留返回值确实令人挠头,同过operator()返回的值是处理链上的最后一个返回的,这么一来,对外部就基本不具可用性了,麻烦得很,倒是可以提供一个vector < Ret > *pRets来返回每个的值,但话说回来,处理函数的返回值在大多数情况下不是很重要
  回复  引用    

#19楼  2005-07-27 09:21 cpunion [未注册用户]
嗯?如果Ret是引用类型,似乎是有问题的,因为C++要求引用必须被初始化,在VC下测试,得到这样一条错误消息:error C2530: 'r' : references must be initialized。引用是可以多次赋值的,你上面那个应该是不存在问题的吧?如果说有问题,可能是说保留最后一次调用的值不太妥当,不过我看到C#中是这么处理的,以前也看到另外一个库这么做。

如果原始类型是A,返回值有可能是A, const A, A&, const A&, A*, const A* (const A* const这种就不考虑了),用那种方式来处理比较容易一些。

不过一般这种东西是不需要返回值的,或者把有返回值的特化版本改为只能委托一个函数,没有委托时调用就抛出异常,这样可能更合理一些。。
  回复  引用    

#20楼  2005-07-27 13:20 问题男 [未注册用户]
首先,是一个小问题,按照目前的写法或是我提议的的写法,在面临const XXX&型的委托时就编译不过,先见此例:
int i = 0, j = 2, k = 3;
int &r = i;
r = j;
r = k;
看上去没什么问题,后面的r都被赋值了,但是r被赋值了还是r上绑定的对象被赋值了呢?仔细观看发现,是i被赋值了,引用类型在初始化后就是那个数据的别名了,没有办法再附着到另一个数据上面

回到委托类,在
for(;;)
ret = call(...)
中因为无法改变const的目标对象,所以const XXX&是不能通过的

不过,不能通过倒也还好,而在XXX&型的委托中,编译没问题,但反倒隐藏了一个大问题

每当运行完ret = call(...)后,实际上是将ret初始化时绑定的对象的内容改变了,而不是为ret赋予新的绑定对象,这个情况就相当严重了,将会给应用埋伏下莫名其妙的隐患

因此,我觉得,应该慎重考虑返回值处理的问题,呵呵
  回复  引用    

#21楼 [楼主] 2005-07-27 14:10 cpunion      
嗯,你说的对,我忘了这是C++基本知识了,引用只能赋值一次,汗。。。。

那么就没有问题了,其它原始类型、引用类型、const引用、指针类型、const指针都可以直接定义一个变量。

暂时不考虑引用类型的返回值,如果有需要,再特化出一个版本罢。。
  回复  引用  查看    

#22楼 [楼主] 2005-07-27 14:52 cpunion      
加上了一个Ret&的特化版本,这个版本不支持+=,它只能委托一个函数,所以只有=操作符。

这个文件是越来越大了,现在已经达到180K,支持的参数个数最大是26个,一般也用不到这么大。如果嫌大,可以修改脚本文件第1行的数字。
  回复  引用  查看    

#23楼 [楼主] 2005-07-28 09:35 cpunion      
嗯嗯。。这种实现有很大的问题,成员函数指针大小不一定是4字节,在VC中,有可能为8、12、16字节,详见“http://www.cstc.net.cn/docs/docs.php?id=309”。

不过我实际测试了一下,确实有这种情况,还真是不好处理,看来不能把它退化成void*指针,被裁切了。

必须要保存原大小的指针,还要保持“如何使用这个指针”的方法,不过光是保持大小还不行,类型信息看来不能丢。

可能恰当的实现,还是要这样:
在每一个参数个数不同特化版本中,提供一个基类:
MethodItemBase
{
virtual Ret call (arguments....);
};
每绑定一个,都要派生一个新类(这个在编译期可以决定,add方法本身是个模板方法,在这个方法里面定义这个类型),所以用模板来实现:
template <T>
struct BoundedMethodItem : public MethodItemBase
{
typedef Ret(T::*mem_fun)(arguments_type);
T* obj;
mem_fun func;
virtual Ret call(...);
};
把这个项目加到调用列表中(vector<MethodItemBase*>),循环调用call即可,效率会降你,不过想不出会有更好的办法。

其它的cdecl处理比较简单:
template CdeclMethodItem: public MethodItemBase
{
typedef Ret (*func_type)(arguments_type);
func_type func;
virtual Ret call(...);
};

还真是麻烦。。。
  回复  引用  查看    

#24楼  2005-07-28 15:34 问题男 [未注册用户]
原来如此,长知识了,拜读过链接的文章后,顺便解释了上回的一个问题,就是为什么g++把
(((NullType *)obj)->*mf)();
译成
004124A9 |> 8B45 FC mov eax, [ebp-4]
004124AC |. 0345 0C add eax, [ebp+C]
004124AF |. 890424 mov [esp], eax
004124B2 |. FF55 F0 call [ebp-10]
,原来他要为this加上一个delta,呵呵,怪不得呢

问题愈发棘手了,虽然只要内存布局单一(不含多张vtbl,基类不含数据成员的多继承,单继承或无继承),就能保证前一版的正确性,但不能保证少数情况还是挺不爽的,呵呵

这种边缘用法,看样子编译器是相当不提倡的,加之规范并没有定义二进制的标准,想要做到兼顾,不是遇难就得遇烦,唉~~~~楼主努力把
  回复  引用    

#25楼  2005-07-28 15:46 问题男 [未注册用户]
虽然可以在上一版再加工:
1、在add_mem_fun函数中,根据sizeof(f)是否大于sizeof(void*)记录下delta(我验证了vc和g++,这两种编译器是把delta存在f大于sizeof(void*)的位置上的,表明偏移的字节数)

2、另一方面,由于在add_mem_fun中将obj专成void*,因此可以确保没有被加上多余的便宜,即可以保证存储的void*指向obj的首部,这样,在call的时候为obj加上这个delta,就能保证正确了

但是,这种做法只可以保证在vc和g++上的正确性。当然,模板的话,灵活性会更强,就是未免繁琐,呵呵
  回复  引用    

#26楼 [楼主] 2005-07-28 15:48 cpunion      
我已经做了一个新的版本,代码量大了,加了好多宏还是挺大。。
为每一个不同参数的版本定义一个方法调用的虚基类:
template <typename Ret, typename.....>
struct MethodItem
{
virtual Ret call (argument types) = 0;
};
接着在上面派生一个CdeclMethod和BoundedMethod模板类:
分别实现函数指针和对象指针的保存,由于是模板类,所以对象和函数指针类型都保存了,只要重写call函数即可。

原来对这2种不同函数指针的判断是用对象指针是否为0来标记,需要一个判断语句,现在不用了,由虚函数完成。

做了一个DelegateBase类来管理指针,也可以用宏插入到各个Delegate类中,暂时没重点考虑这个。

目前测试来看还算不错,可能会有虚函数调用成本,但原来的版本就不快,所以也无所谓了。

这样做以后有个新问题:Delegate<void(int)>无法委托void(int)const函数,因为它们类型不同,也无法转换,不过我认为可能会这样用。比如Delegate类的operator()本身是一个const函数,把一个委托委托给另一个委托应该是允许的,因为我的目标是让它能接受仿函数,而委托自身也是个仿函数。前面这个版本的处理方式是强行转为void*再调用,这次是用一个union做了转换。

我把代码发上来。
  回复  引用  查看    

#27楼 [楼主] 2005-07-28 15:56 cpunion      
另有一个问题:
Delegate类声明如下:
template <typename T>
class Delegate
{
};
偏特化:
template<Ret>
class Delegate<Ret()>
{
};
template<>
class Delegate<void()>
{
};
这是类可以编译的,不过另外2个辅助类却提示错误,最后只好又加了个参数才解决掉。(把Ret参数提出来再做成一个模板参数,如下,还是用这个类说明:)
template<Ret>
class Delegate<Ret, Ret()>
{
};
template<>
class Delegate<void, void()>
{
};
这样就没有问题。
  回复  引用  查看    

#28楼  2005-07-28 16:17 问题男 [未注册用户]
使用模板的话,烦就不可避免了

另外,我觉得这个话题其实还是相当有意思的,但这里主要关注.net,从文章及回帖看来,懂得并喜爱c++的鲜有人在,十个来看恐怕八个都摸不着头脑,更谈不上兴趣了,您可以把这个发到c++气氛更浓的地方去讨论,被关注程度应该更高,呵呵
  回复  引用    

#29楼 [楼主] 2005-07-28 16:21 cpunion      
申请了一堆blog,不过大部分都无法正常访问,特别是csdn的。这个虽然慢点,总算可以访问。。。

你有没有blog?我想做个链接。。。
  回复  引用  查看    

#30楼  2005-07-28 16:23 问题男 [未注册用户]
没有,抱歉

btw:这里的速度还是相当快的,硬件环境还是很好的,呵呵
  回复  引用    

#31楼 [楼主] 2005-07-28 17:36 cpunion      
那篇FastDelegate,他是模拟了各种编译器的对象模型,包括不同版本,从这个结构中找到真正的函数地址,然后保存对象地址和这个函数地址,原理上和我们开始那个版本差不多,只是计算出来的函数地址是正确的。又因为不是多分派的,所以他也fast。。。
  回复  引用  查看    

#32楼 [楼主] 2005-08-04 15:27 cpunion      
今天才看到Boost.Signals libsigc++ 这2个库,实现了我这里的功能,它的返回值是放入一个vector的。。有空再细看
  回复  引用  查看    





标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2005-07-28 15:52 编辑过
Google站内搜索

相关文章:

相关链接: