2013年6月23日 星期日

C++ 的 delegate

C++ 是 OOL 沒錯,但卻沒有近代 OOL 常用到的 delegate 語法,這導致有些 Delegation Pattern 在 C++ 上面難以實作得很到位,蠻多文章都會示範如何利用 Interface 來模擬 Delegation Pattern 的方式,但還是有不少令人看起來覺得怪怪的程式碼,前陣子在看一個 OpenSource 專案,名叫 FastDelegate

Open Source 的領域雖然大家都是純著佛心在分享的,但使用的人其實存在很多中二,所以在給評價這回事上和應用程式市集上是很相似的,評價和評論往往很多讓人看了搖頭的發表,在 Code Project 上也一樣,但這個專案卻可以拿到 6xx 人次平均 4.98 顆星的評價,果然厲害,這篇文章的內容很有趣,講了不少編譯器在 function point 與 member function point 的故事

我大概提幾個有趣的重點:
1. There are some weird things about member function pointers. Firstly, you can't use a member function to point to a static member function. You have to use a normal function pointer for that.
靜態函式也是一個類別的 member function,但 member function point 只能指向一般的函數,不能指向靜態的。

2. When dealing with derived classes, there are some surprises. You can safely cast a member function pointer from a derived class to its first base class only!
其實 member function point 還可以指向父類別,但僅能是多重繼承的第一個基底類別,前幾代 C++ 其實只能單一繼承,後來某個版本後才支援多重繼承,但 memeber function point 卻因實作困難無法突破這個限制。

3. There's another interesting rule in the standard: you can declare a member function pointer before the class has been defined.
即便這個類別未完成,member function point 亂宣告也不會造成編譯不過。

4. They use a horrible hack, putting all the possible member function pointers into an enormous union, to subvert the normal C++ type checking.
裡面提到 MFC Library 的怪招,可以看看 afximpl.h 這個檔案,裡面把所有視窗所有需要用到的 function point 原型全宣告在這,搭配第 3 點一起看,再看看 DECLARE_MESSAGE_MAP()、BEGIN_MESSAGE_MAP()、END_MESSAGE_MAP() 這些 Macro 可以了解到 MFC 中 OnCreate、OnPaint 這些 Event Handler 的運作方法,相當有趣。

5. You'd probably guess that a "member function pointer", like a normal function pointer, just holds a code pointer. You would be wrong.
大家可能會以為 member function point 就像一般的 point 一樣是 4 個 Bytes,但其實幾乎所有編譯器的 member function point 都是大於 4 Bytes 的,且各編譯器實作方法不同,有些 8、有些 16,且參數的量還會影響這個 point 的尺寸,不只有跨平台問題,還有跨編譯器問題。

這個 FastDelegate 包含了泛型類別及成員函式指標的語法概念,比較需要注意的是,這個 Class 並不完全符合 C++ 的標準,所以在某篇編譯器下會無法編譯,所幸如果是使用 MSBuild (Visual Studio) 在編譯程式,那這個專案正好適合拿來用。

以下做一個小示範,示範一個 Dialog 介接 REST API 的 Login 流程,從程式碼和類別的切分可以輕易看出 delegate 在程式寫作上帶來的好處及程式碼的清晰感,範例中我示範的動作是一個 Button 按下的 UI 動作觸發一段 Background 的流程,我將 LoginInfo 與 LoginAPI 各做成一個類別,LoginAPI 啟動後 UI 可以選擇要不要透過 bind 的方式來接受 LoginAPI 的委派,我做了兩個事件,一個是開始登入、一個是登入完成,通常我們常做的就是在這兩種情況 Show 與 Hide Progressing Dialog…

LoginInfo.h 部份
#pragma once

class CLoginInfo
{
public:
int Status;
CString Session;
CString Message;
};

LoginAPI.h 部份
#pragma once

#include "LoginInfo.h"
#include "FastDelegate.h"

using namespace fastdelegate;

class CLoginAPIParam
{
public:
CString UserId;
CString UserPasswordMD5;
};

class CLoginAPI
{
public:
CLoginAPI(void);
~CLoginAPI(void);

typedef FastDelegate2<CLoginAPI*, CLoginInfo> LoginAPIEvent;
LoginAPIEvent LoginAPIStarted;
LoginAPIEvent LoginAPICompleted;
BOOL IsCancel;

void GetResultAsync(CLoginAPIParam param);
void GetResultCore();

private:
CLoginAPIParam APIParam;
};

UINT LoginThread(LPVOID pParam);

LoginAPI.cpp 部份
#include "StdAfx.h"
#include "LoginAPI.h"

CLoginAPI::CLoginAPI(void)
{
IsCancel = FALSE;
}


CLoginAPI::~CLoginAPI(void)
{
}

void CLoginAPI::GetResultAsync(CLoginAPIParam param)
{
APIParam = param;
AfxBeginThread(LoginThread, (LPVOID)this, THREAD_PRIORITY_NORMAL);
}

UINT LoginThread(LPVOID pParam)
{
CLoginAPI *pApi = (CLoginAPI*)pParam;
pApi->GetResultCore();
return TRUE;
}

void CLoginAPI::GetResultCore()
{
if (!LoginAPIStarted.empty())
{
LoginAPIStarted(this, CLoginInfo());
}

CString url;
url.Format(_T("http://login.kkbox.com/login.php?kkid=%s"), _T("KKID"));
CString post;
post.Format(_T("uid=%s&pwd=%s"), APIParam.UserId, APIParam.UserPasswordMD5);

// 偽裝連線取 HttpResponse 開始
CLoginInfo infoResult;
if(!IsCancel)
{
Sleep(5000);
infoResult.Status = 2;
infoResult.Message = _T("Login Success");
infoResult.Session = _T("v00001156argsgasfas345354era3ef4ag5varg35s4vg16s3");
}
// 偽裝連線取 HttpResponse 結束

if (!LoginAPICompleted.empty())
{
LoginAPICompleted(this, infoResult);
}
}

EasyDelegateDlg.h 部份
#pragma once

#include "LoginInfo.h"
#include "LoginAPI.h"

class CEasyDelegateDlg : public CDialogEx
{
public:
CEasyDelegateDlg(CWnd* pParent = NULL);

void OnLoginAPIStarted(CLoginAPI *sender, CLoginInfo info);
void OnLoginAPICompleted(CLoginAPI *sender, CLoginInfo info);

enum { IDD = IDD_EASYDELEGATE_DIALOG };

protected:
virtual void DoDataExchange(CDataExchange* pDX);

protected:
HICON m_hIcon;
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
DECLARE_MESSAGE_MAP()

public:
afx_msg void OnButtonClicked();
};

EasyDelegateDlg.cpp 部份
#include "stdafx.h"
#include "EasyDelegate.h"
#include "EasyDelegateDlg.h"
#include "afxdialogex.h"

CEasyDelegateDlg::CEasyDelegateDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(CEasyDelegateDlg::IDD, pParent)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CEasyDelegateDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CEasyDelegateDlg, CDialogEx)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_BUTTON, &CEasyDelegateDlg::OnButtonClicked)
END_MESSAGE_MAP()

void CEasyDelegateDlg::OnButtonClicked()
{
CLoginAPIParam param;
param.UserId = _T("asciiss@gmail.com");
param.UserPasswordMD5 = _T("a52b3710e215d6547a59c7253e47a9d2");

CLoginAPI *api = new CLoginAPI;
api->LoginAPIStarted.bind(this, &CEasyDelegateDlg::OnLoginAPIStarted);
api->LoginAPICompleted.bind(this, &CEasyDelegateDlg::OnLoginAPICompleted);
api->GetResultAsync(param);
}

void CEasyDelegateDlg::OnLoginAPIStarted(CLoginAPI *sender, CLoginInfo info)
{
// 顯示 "登入中..." 的字樣或對話框
}

void CEasyDelegateDlg::OnLoginAPICompleted(CLoginAPI *sender, CLoginInfo info)
{
delete sender;

// 關閉 "登入中..." 的字樣或對話框
}

用真正的 Delegation Pattern 我最喜歡的好處在於要接事件就接,不想接就不要做 bind (或稱 subscript),在我寫的範例中 CEasyDelegateDlg 可以選擇要不要 bind CLoginAPI 的 LoginStarted 與 LoginAPICompleted 的兩個事件,若不做 bind 的動作,CEasyDelegateDlg 中就不需要處理任何 LoginAPI 的事件,也就不需要 OnLoginAPIStarted 與 OnLoginAPICompleted 兩個 Function 的存在,而且 CLoginAPI 可以透過 empty 這個 Function 知道自己到底有沒有需要 call 委派對像,此外若 CLoginAPI 有類似 ProgressChanged 的事件,CEasyDelegateDlg 也可以在接獲事件時,決定動作是否該停止,減少一些在 CLoginAPI 中傳入布林指標的寫法,整體相當的方便清晰。

沒有留言:

張貼留言