browser icon
You are using an insecure version of your web browser. Please update your browser!
Using an outdated browser makes your computer unsafe. For a safer, faster, more enjoyable user experience, please update your browser today or try a newer browser.

球胡麻差

Posted by on 2005 年 09 月 14 日

你可以任意转载本文,但请在转载后的文章中注明作者和原始链接。
媒体约稿请联系 titilima_AT_163.com(把“_AT_”换成“@”)。

按:球胡麻差,山西方言,乱七八糟之意。

C++对于托管代码的封装一向不是很尽善尽美,从最初的static成员函数到MFC的消息映射表,及至ATL的thunk机制。真可谓花样百出、层出不穷了。究其原因,这乃是C++的this指针惹的祸,这个“祸害”也就是Borland的VCL是用Object PASCAL编写的,而C++ Buider只能提供VCL的动态链接之缘由了。

然而,我在不经意之间却获得了另一个封装的方法,完全脱离了static成员函数的一贯做法,并直接将非static成员函数指定为线程的托管代码——也许这听上去很神奇,其实不过尔尔,且听李马慢慢道来。

首先我将线程对象封装成一个纯虚基类ThreadObject,如下:

class ThreadObject
{
public:
    virtual void Create() = 0;
    void Wait()
    {
        WaitForSingleObject( m_hThread, INFINITE );
        CloseHandle( m_hThread );
    }
protected:
    virtual DWORD WINAPI DoWork( void )
    {
        for ( int i = 0; i < 10; i++ )
        {
            Sleep( rand() % 1000 );
            printf( "Thread %08X is running. ", m_dwThreadID );
        }
        return 0;
    }
    DWORD m_dwThreadID;
    HANDLE m_hThread;
};

这个类简单地封装了线程对象的数据成员及工作函数,下面我将基于这个类使用C++的继承来实现两种不同的托管封装。

首先是通常使用的方法。这种方法使用了一个static成员函数作为线程的托管代码,在创建线程的时候将类的this指针传入作为线程参数,代码大致如下:

class MyThread1 : public ThreadObject
{
public:
    void Create()
    {
        m_hThread = CreateThread( NULL, 0, MyThread1::m_ThreadProc, this, 0, &m_dwThreadID );
    }
protected:
    static DWORD WINAPI m_ThreadProc( LPVOID lpParam )
    {
        MyThread1 *pThis = (MyThread1 *)lpParam;
        return pThis->DoWork();
    }
};

下面我来解释一下使用static成员函数的原因,也就是开头所说的“this指针惹的祸”。CreateThread所需要的线程入口函数是一个这样规格的函数:

DWORD WINAPI ThreadProc( LPVOID lpParameter );

如果使用了非static成员函数(诸位可以将m_ThreadProc前面的static去掉重新编译试试),那么编译器会给出类似这样的出错提示:

error C2664: ‘CreateThread’ : cannot convert parameter 3 from ‘unsigned long (void *)’ to ‘unsigned long (__stdcall *)(void *)’

这是为什么呢?其实,C++的非static成员函数在编译器的处理下,会在参数中加入一个隐含的this指针,成为类似这个样子:

DWORD WINAPI MyThread1_m_ThreadProc( const MyThread1* this, LPVOID lpParam );

这当然不符合我们预期的调用约定。于是,严格的C++编译器就会在发生类似这样的类型转换的时候予以坚决制止。不过,当我回头望到基类中的这个函数的时候,突然眼前一亮:

DWORD WINAPI ThreadObject::DoWork( void );

我想,这个函数经过this指针处理后,应该会变成类似这个样子:

DWORD WINAPI ThreadObject_DoWork( const ThreadObject* this );

一个指针参数,这倒是非常符合线程函数的规格了。于是,我写出了如下的代码:

LPVOID p = (LPVOID)DoWork;
LPTHREAD_START_ROUTINE pFunc = (LPTHREAD_START_ROUTINE)p;
m_hThread = CreateThread( NULL, 0, pFunc, this, 0, &m_dwThreadID );

结果令人失望,因为编译器根本不允许将DoWork转换成LPVOID。百无聊赖之中,我随手写下了这样的代码:

LPTHREAD_START_ROUTINE pFunc = (LPTHREAD_START_ROUTINE)0x12345;

这段代码竟然能够编译成功(不过当然不能执行,否则程序必然当掉),于是,我将目光移到了虚函数表上。我可以通过this指针获取虚函数表指针vptr的值,然后经由这个指针获得虚函数表,那么这个表的第二个栏位自然就是DoWork的地址了!于是我重新振作起来,完成了我的线程类:

class MyThread2 : public ThreadObject
{
public:
    void Create()
    {
        // 首先获得vtable的指针vptr
        DWORD **pVptr = (DWORD **)this;
        // 经由虚函数表获得DoWork的地址进行调用
        LPTHREAD_START_ROUTINE pFunc = (LPTHREAD_START_ROUTINE)(*pVptr)[1]; // (*pVptr)[0]为Create
        m_hThread = CreateThread( NULL, 0, pFunc, this, 0, &m_dwThreadID );
    }
};

那么,现在可以对比测试一下了:

MyThread1 t1;
MyThread2 t2;
t1.Create();
t2.Create();
t1.Wait();
t2.Wait();

这就是我花了半个下午的时间封装出来的代码。走笔至此,我突然问自己:这半个下午我到底做了什么?就是这么一段非常有暴力倾向甚至有些变态的代码吗?呃……的确是这样,因此我还是建议你使用MyThread1的托管封装做法。至于我的做法,我仍然希望它能多少带给你一些启发或警示,使得它还不至于完全没用。

真是球胡麻差

订阅本站

6 Comments

  • At 2006.02.23 13:41, 四不象 said:

    你难道就不考虑多重继承的情况了么?

    • At 2006.02.23 14:29, 李马 said:

      To 四不象:
      请赐教。

      • At 2006.02.24 16:13, 四不象 said:

        你的代码基于假设 MyThread2::Create() 和 MyThread2::DoWork() 两个成员函数中的 this 指针是一致的。
        一般情况下,这两个成员函数内部的this指针确实是一致的,但是对于多重继承或者虚继承,基类的成员函数内部this指针和派生类的this指针并不一致。

        看如下的代码:

        class base1
        {
        public:
        int m_fill1;
        void func1(){printf(“0x%08X\n”,this);};
        };

        class base2
        {
        public:
        int m_fill2;
        void func2(){printf(“0x%08X\n”,this);};
        };

        class derived:public base1,public base2
        {
        public:
        int m_file3;
        };

        int main()
        {
        derived test;
        test.func1();
        test.func2();
        return 0;
        }

        • At 2006.02.24 16:48, 李马 said:

          To 四不象:
          感谢您的提醒。MyThread2封装的局限性,并非仅仅是在于您所说的多重继承,参看:
          LPTHREAD_START_ROUTINE pFunc = (LPTHREAD_START_ROUTINE)(*pVptr)[1]; // (*pVptr)[0]为Create
          根据vtable表的栏位来调用一个成员函数,这是一句我自己都认为有些变态的代码。假如ThreadObject面临日后的扩展,那么修改这一句的方法是非常不可行的。所以,这段代码是完全不可用的,它没有任何存在的理由。这也就是我没有对它深究的原因。
          当然,回到多重继承的问题,我为之做一个答复:
          class derived : public base1, public base2
          {
          public:
          int m_file3;
          void func3()
          {
          printf( “0x%08X\n”, (const base1*)this );
          printf( “0x%08X\n”, (const base2*)this );
          }
          };

          • At 2006.02.28 16:32, mopyman said:

            C++对于”托管代码”的封装一向不是很尽善尽美,
            你这里说的托管代码指什么?看你的代码是对win32线程API的封装,好像没有涉及到托管代码。

            • At 2006.02.28 16:43, 李马 said:

              To mopyman:
              这里所说的托管封装,是指C++类对Win32回调函数的封装。

              (Required)
              (Required, will not be published)