最近写一个使用完成端口的应用时, 居然在最简单的类型转换上栽了一个跟头, 写出来与大家分享,以避免犯和我类似的错误。
为了能尽量统一的处理每一个io操作, 我定义了下面这个类:
class CIoPacket : public OVERLAPPED
{
public:
virtual void OnIoComplete( ULONG_PTR key, DWORD dwBytes ) = 0;
virtual void OnIoFailure( ULONG_PTR key, DWORD dwBytes ) = 0;
};
其中的两个虚函数分别在io操作成功完成或失败后被调用. 它的派生类将用于记录每一次io的相关信息. 为了从网络上接收数据, 我有从它派生出了一个类:
class CNetPkt : public CIoPacket
{
protected:
// 定义一些成员变量
public:
virtual void OnIoComplete( ULONG_PTR key, DWORD dwBytes )
{
// 执行一些操作
}
virtual void OnIoFailure( ULONG_PTR key, DWORD dwBytes )
{
// 执行一些操作
}
}; 下面的代码启动了一个接受数据的操作:
//
SOCKET sck;
// 初始化sck并绑定到一个完成端口
CNetPkt* pPkt = new CNetPkt();
// 设置pPkt的相关字段
::WSARecv( sck, &wsabuf, 1, &dwBytes, &dwFlags, pPkt, NULL ); // (0)
// 下面是完成端口线程中的代码:
DWORD dwBytes = 0;
ULONG_PTR key = 0;
LPOVERLAPPED pol = NULL;
if( ::GetQueuedCompletionStatus(g_hIocp, &dwBytes, &key, &pol, INFINITE) )
{
if( key != 0 )
reinterpret_cast<CIoPacket*>(pol)->OnIoComplete( key, dwBytes ); // (1)
}
else
{
if( pol != NULL )
reinterpret_cast<CIoPacket*>(pol)->OnIoFailure( key, dwBytes ); // (2)
} 自我感觉实现的既灵活又漂亮还健壮、高效(嗯?!哪里来的西红柿?味道还不错!). 但是程序每次运行到(1)或(2)时就会出现非法操作, 令我百思不得其解. 首先仔细检查程序, 没发现错误; 又使出十八般调试功夫, 还是没有找到问题所在. 正在头大之时, 突然发现(1)处的pol和(0)处的pPkt的值并不一样, pol比pPkt大了4, 进一步通过反汇编发现(0)处实际传给WSARecv的就是((LPBYTE)pPkt)+4. 两个值不一样, 总出错也就不奇怪了.
可编译器为什么要给指针加上4呢? 难道是编译器把CIoPacket的vfptr放到了OVERLAPPED的前面? 可是我记得vc应该是把它放在后面的呀(具体不敢确定了, 但好像VC6就是放在后面). 一番测试证实了我的猜测, vc7.1就是会把vfptr放到类结构的最前面, 该死的微软居然偷偷改了这么重要的编译细节. 但也不能光骂微软, 自己的错误也要检讨一下, 上面的程序中我应该用static_cast, 而不是reinterpret_cast, 因为static_cast能正确调整基类和派生类的指针, 而reinterpret_cast从汇编的角度看是什么也不干的. 如果vfptr放在后面, static_cast和reinterpret_cast的结果是一样的, 但当vfptr放在前面的时候它们就不同了. 由于C++标准没有规定vfptr的放置位置, 所以大家进行类型转换时一定要注意选择正确方式, 避免出现我这样的、隐蔽的可移植性问题.
re: reinterpret_cast和static_cast
2005-07-26 21:16
ms的编译器虚函数表指针都在前面,没有放后面的
也基本可以确定没有编译器会放后面,简而言之,放后面还要知道类型大小才能正确定位虚函数表,会自找麻烦
re: reinterpret_cast和static_cast
2005-07-27 00:00
vfptr的放置是厂商定义的, 标准并没有规定放在那.
reinterpret_cast是按位转换, 本来就应慎重使用.
re: reinterpret_cast和static_cast
2005-07-27 08:08
我一般不会把CIoPacket做成虚类
re: reinterpret_cast和static_cast
2005-07-27 08:56
确实应该用static_cast,但问题其实出在
class CIoPacket : public OVERLAPPED
{
virtual……
};
这一句,因为OVERLAPPED不是虚基类,而CIoPacket是。
re: reinterpret_cast和static_cast
2005-07-27 12:57
LPOVERLAPPED pol;
改为
CIoPacket *pol;
说实话,我很奇怪,为什么大家都喜欢这种奇怪的用法,然后不停的去过于追寻编译器的细节内幕。
re: reinterpret_cast和static_cast
2005-07-27 13:21
首先声明一下只讨论单继承,多继承、虚继承太复杂了
to 问题男:
把vfptr放在后面的编译器也很多(准确的说是第一个有虚函数的类的最后面), 因为放在前面编译器要处理许多类似文中的指针转换(自动增减一定的偏移), 同样也很麻烦.
to SevenCat:
非常想知道你的处理方法, 我要处理很多的异步io操作(有socket也有file),每个操作完成后的处理又各不相同(甚至到项目后期还会有更多不同的操作),我觉得我现在的方案是比较好的,但也一直在寻找更好的
to 周星星:
没太看明白你的意思
to 清风雨:
我觉得这并不是什么奇怪的用法,这只是一个从基类指针到派生类指针的转换,很正常。虽然更正常的是从派生类到基类的转换,但一个完整的项目不可能总工作在理想状态下,必要时用一些“奇计淫巧”会很管用。
而且我觉得不了解编译器的细节内幕, 根本就不可能写出好的程序
re: reinterpret_cast和static_cast
2005-07-27 15:38
我来给你解释 周星星 的话。
就是OVERLAPPED里面没有虚函数表,所以用虚函数访问时不对了。
我的意思是,如果你直接用CIoPacket *了,也不会出问题。为什么要你直接用CIoPacket *而不是LPOVERLAPPED呢?因为你用到的就不是LPOVERLAPPED的(从你的转型可知),所以也就不需要转型了。个人觉得:不用的就确切的不要用(这里就是是OVERLAPPED而不是CIoPacket的对象实例)。
re: reinterpret_cast和static_cast
2005-07-27 18:23
各个人的设计都不一样
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytes,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED* lpOverlapped,
DWORD dwMilliseconds
);
这里面还有个lpCompletionKey,这里面也可以放一个CHandler指针。
lpOverlapped是per io的,CHandler是per handle的东东。
在WRITE的发送成功后,基本不必考虑处理,
在READ的读接收的时候,这个PACKET取回的数值也基本无意义,因为TCP本身就是沾包的。
还有人的设计(好像包括APACHE)是这样的:
class AsyncResult : public OVERLAPPED
{
直接放在这个东东里面。
private:
CHandler *_handler;
}
反正怎么做都行,运用之妙,存于一心。
re: reinterpret_cast和static_cast
2005-07-27 18:24
to清风雨:
这里的确是CIoPacket的实例。只不过偏移量错了
re: reinterpret_cast和static_cast
2005-07-27 19:39
to清风雨:
这一点你就冤枉我了,不是我不想直接用CIoPacket*, 而是我不能用。你可能不了解windows的异步io,可以启动异步io的api(如WSARecv, ReadFile等)都要求一个LPOVERLAPPED类型的参数,所以我必须给它。
to SevenCat:
你的设计对我可能不太适合,因为对我来说,同样是从网上接收数据,但接收后处理却可能不一样,我的设计是每种处理定义一个CIoPacket的子类,然后调它的OnIoComplete时自然就能进行正确的处理了。
re: reinterpret_cast和static_cast
2005-07-27 20:05
你接收数据并不能保证每次的包接收都是正好你想接收包的长度,他会粘包的,你还要折包才行。
re: reinterpret_cast和static_cast
2005-07-28 08:31
这个对我应该不是问题,因为我能保证发送数据的那一方每次只发送一包数据, 然后就等待我的应答。而每包数据的最前面都有这个数据包的长度,我只要处理被tcp拆包的情况就行了
re: reinterpret_cast和static_cast
2005-07-28 09:09
CIoPacket* 就是 LPOVERLAPPED
比如:
fun( base *obj );
可以fun( &derived );调用
或者可以在调用时转型。
re: reinterpret_cast和static_cast
2005-07-28 09:37
to 清风雨:
是呀,你说的没错,所以我在(0)处用的就是pPkt。
但这样做实际上是把转型的任务交给编译器了,那一句相当于:
::WSARecv( sck, &wsabuf, 1, &dwBytes, &dwFlags, static_cast(pPkt), NULL );
而就是这个static_cast把pPkt加上了4字节的偏移,导致我在(1)和(2)处必须再用static_cast减去4字节的偏移.
我原来的错误就在于一边用的是static_cast另一边用的是reinterpret_cast,如果vfptr放在后面,这两个cast就是等价的,而vfptr放在前面,它们就不等价。但vfptr放在哪c++标准并没有规定,所以我说原来的错误实际上可以归到可移植性问题上去。
re: reinterpret_cast和static_cast
2005-08-12 10:11
我不知道你为什么就一定要用reinterpret_cast这个玩意?你就不能不用它吗。
re: reinterpret_cast和static_cast
2006-01-25 11:23
是不是用dynamic_cast进行转换更好
to 只说实话
2006-01-25 21:11
你能告诉我怎么不用吗,如果我想把程序写得更规范的话?
实际的程序中不用它是不可能的,而他相比c样式的强制类型转换是有优势的,所以c++才会引入四个类型转换符,并建议程序员不要使用c样式的类型转换
to 111
2006-01-25 21:14
dynamic_cast效率太低,而且这里没必要,因为我明确的知道我正在转换的基类指针是指向一个派生类
re: reinterpret_cast和static_cast
2006-07-04 17:15
可以不用reinterpret_cast吗?
为什么编一个一模一样的程序,一个没用reinterpret_cast就报错,一个却可以通过?是不是可以通过什么设置忽略这个错误呢?
re: reinterpret_cast和static_cast
2007-08-09 10:43
也学了很长一段时间的C++了,但很用使用这个类型转换,一般是使用C样式的,现在看来学问多多啊,多谢各位的言论了。
re: reinterpret_cast和static_cast
2007-09-20 21:26
vc 6 vfptr是放在前面的.
re: reinterpret_cast和static_cast
2007-09-20 21:40
我知道的所有的编译器都是放在前面.
--------------------next---------------------