C++開發與Windows API


Windows API 向 C++ 開發人員提出了一項挑戰。 組成 API 的眾多庫大都表現為 C 語言風格的函數和句柄或是 COM 風格的接口。 這些用起來都不太方便,需要進行一定的封裝或間接操作。

C++ 開發人員的難題是如何確定合理的封裝級別。 與 MFC 和 ATL 這樣的庫一起成長起來的開發人員可能傾向於將所有內容都包裝為類和成員函數,因為這是他們長久以來依靠的 C++ 庫所表現出的模式。 也有些開發人員可能對任何形式的封裝都嗤之以鼻,而只是直接使用原始函數、句柄和接口。 可以說這部分開發人員不是真正的 C++ 開發人員,而只是有身份問題的 C 開發人員。 我相信,現在的 C++ 開發人員有着更為自然的中間立場。

我在 MSDN 雜志 重新開始了我的專欄,我在此將向您展示如何使用 C++0x(很可能會命名為 C++ 2011),以及如何使用 Windows API 將本機 Windows 軟件開發從黑暗時代解救出來。 接下來幾個月,我將帶您更深入體驗 Windows 線程池 API。 在這個過程中,您會看到如何編寫極具可擴展性的應用程序,而無需使用花哨的新語言以及復雜或昂貴的運行時。 您只需要有優秀的 Visual C++ 編譯器、Windows API 和掌握技巧的願望就足夠了。

像所有好項目一樣,良好的基礎是成功的一半。 那么,我要如何“包裝”Windows API 呢? 我不想在后面每個專欄中拘泥於這些細節,因此打算在本專欄中講清楚建議的做法,在以后依此執行。 有關 COM 風格接口的問題暫不做討論,因為下面幾個專欄還用不到這種接口。

Windows API 由很多庫組成,這些庫公開一組 C 語言風格的函數,以及一個或多個稱為句柄的不透明指針。 這些句柄通常表示庫或系統資源。 有相應的函數可用於創建、操作和釋放使用句柄的資源。 例如,CreateEvent 函數創建一個事件對象,並返回一個到該事件對象的句柄。 要釋放該句柄並告知系統您已使用完該事件對象,只需將該句柄傳遞給 CloseHandle 函數即可。 如果同一事件對象再無其他現用句柄,系統將銷毀該對象:


auto h = CreateEvent( ...
);
CloseHandle(h);

C++ 新手
如果您是初次接觸 C++ 2011,我要指出一點:auto 關鍵字會告知編譯器根據初始化表達式推斷變量類型。 這在您不知道表達式類型時非常有用,在進行元編程或只想保存一些擊鍵時,常常會出現這種情況。

但您幾乎在任何時候都不應編寫這樣的代碼。 毫無疑問,C++ 提供的最有價值功能就是類的功能。 模板很酷,標准模板庫 (STL) 很神奇,但如果沒有類,C++ 中的一切都毫無意義。 C++ 程序簡明可靠的優點要歸功於類。 我說的不是虛函數、繼承和其他花哨的功能。 我說的只是構造函數和析構函數。 這往往就是您所需要的一切,還有, 這不需要付出任何成本。 在實踐中,您需要了解異常處理的開銷,本專欄末尾將討論這個問題。

要馴服 Windows API 並使其為 C++ 開發人員所用,需要一個封裝句柄的類。 是的,您所喜愛的 C++ 庫已經有一個句柄包裝,但它是完全為 C++ 2011 設計的嗎? 您能放心地將這些句柄存儲在 STL 容器中,然后在程序中傳遞它們並跟蹤其所有者嗎?

C++ 類是完美的句柄抽象。 請注意,我沒有說“對象”。要記住,句柄是對象在程序中的代表,往往不是對象本身。 需要看管的是句柄,而不是對象。 Windows API 對象與 C++ 類之間若存在一對一關系有時會非常方便,不過這是另外一個問題。

雖然句柄一般是不透明的,但仍會存在不同類型的句柄,而且往往有微妙的語義區別,這就有必要使用類模板以常規方式對句柄充分進行包裝。 需要使用模板參數來指定句柄類型以及句柄的具體特性或特征。

在 C++ 中,特征類通常用於提供關於給定類型的信息。 這樣,我可以為多個句柄編寫一個類模板,並為 Windows API 中不同類型的句柄提供不同的特征類。 句柄的特征類還需要定義句柄的釋放方式,以使句柄類模板能夠根據需要自動釋放句柄。 例如,下面就是事件句柄的一個特征類:


struct handle_traits
{
static HANDLE invalid() throw()
{
return nullptr;
}

static void close(HANDLE value) throw()
{
CloseHandle(value);
}
};

因為 Windows API 中的很多庫共享這些語義,所以它們不單用於事件對象。 如您所見,特征類只包含靜態成員函數。 如此一來,編譯器即可輕松嵌入代碼而不引入任何開銷,同時為元編程提供極大的靈活性。

無效函數返回無效句柄的值。 這個值通常為 nullptr,是 C++ 2011 中的一個新關鍵字,表示 null 指針值。 不同於傳統的同類值,nullptr 是強類型的,因此適用於模板和函數重載。 無效句柄有時也定義為非 nullptr 的值,這就會導致特征類中包含無效函數。 close 函數封裝關閉或釋放句柄的機制。

給出了特征類的輪廓,我可以繼續並開始定義句柄類模板,如圖 1 所示。

圖 1 句柄類模板


template <typename Type, typename Traits>
class unique_handle
{
unique_handle(unique_handle const &);
unique_handle & operator=(unique_handle const &);

void close() throw()
{
if (*this)
{
Traits::close(m_value);
}
}

Type m_value;

public:

explicit unique_handle(Type value = Traits::invalid()) throw() :
m_value(value)
{
}

~unique_handle() throw()
{
close();
}

我將它命名為 unique_handle,因為它與標准的 unique_ptr 類模板有些神似。 很多庫還使用相同的句柄類型和語義,因此有必要為最常用的情況提供一個 typedef,簡單地叫它 handle 就好了:


typedef unique_handle<HANDLE, handle_traits> handle;

現在,我可以創建一個事件對象並將其聲明為“handle”,如下所示:


handle h(CreateEvent( ...
));

我已將 copy 構造函數和 copy 賦值運算符聲明為私有,並且保持它們未實現。 這會阻止編譯器自動生成它們,因為它們很少適合句柄。 Windows API 允許復制特定類型的句柄,但這是與 C++ copy 語義非常不同的概念。

構造函數的值參數依靠特征類提供默認值。 析構函數調用私有的 close 成員函數,該函數又依靠特征類根據需要關閉句柄。 這樣,我就得到了一個堆棧友好且異常安全的句柄。

不過這些還不夠。 close 成員函數依靠 Boolean 轉換來確定是否需要關閉句柄。 雖然 C++ 2011 引入了顯式轉換函數,但在 Visual C++ 還沒有這樣的函數,因此我使用一種通用的 Boolean 轉換方法來避免編譯器在正常情況下允許的令人擔心的隱式轉換:


private:

struct boolean_struct { int member; };
typedef int boolean_struct::* boolean_type;

bool operator==(unique_handle const &);
bool operator!=(unique_handle const &);

public:

operator boolean_type() const throw()
{
return Traits::invalid() != m_value ?
&boolean_struct::member : nullptr;
}

這意味着我現在可以簡單地測試句柄是否有效,而不會允許危險的轉換暗地進行:


unique_handle<SOCKET, socket_traits> socket;
unique_handle<HANDLE, handle_traits> event;

if (socket && event) {} // Are both valid?
if (!event) {} // Is event invalid?
int i = socket; // Compiler error!
if (socket == event) {} // Compiler error!

使用更明顯的布爾運算符會允許最后兩個錯誤暗地發生。 雖然如此,這的確會允許兩個套接字之間的比較,因此需要顯式實現相等運算符或將它們聲明為私有並保持未實現。

unique_handle 擁有句柄的方式與標准 unique_ptr 類模板擁有對象並通過指針管理對象的方式相似。 因此,可以通過提供我們所熟悉的 get、reset 和 release 成員函數來管理基礎句柄。 get 函數非常簡單:


Type get() const throw()
{
return m_value;
}

reset 函數稍微復雜,但我們已經討論過它的構建基礎:


bool reset(Type value = Traits::invalid()) throw()
{
if (m_value != value)
{
close();
m_value = value;
}

return *this;
}

我冒昧對 unique_ptr 提供的 reset 函數模式做了輕微的改動,使其返回一個布爾值,指示對象是否已用有效句柄進行了重置。 這樣方便進行錯誤處理,稍候我們再討論這個問題。 release 函數現在顯而易見:


Type release() throw()
{
auto value = m_value;
m_value = Traits::invalid();
return value;
}

復制與 移動
最后是考慮復制與移動語義。 因為我已經禁止了句柄的復制語義,所以應該允許移動語義。 如果要在 STL 容器中存儲句柄,這一點非常重要。 這些容器在傳統上依賴復制語義,但隨着 C++ 2011 的引入,開始支持移動語義。

這里我不詳細介紹移動語義和 rvalue 引用,只是告訴大家,其基本理念是允許對象值以開發人員可預測並且對於庫作者和編譯器一致的方式在對象間傳遞。

在 C++ 2011 之前,開發人員不得不求助於各種復雜的技巧來避免語言(廣義上講為 STL)對復制對象的過度喜愛。 編譯器常常創建一個對象副本,然后立即銷毀原始對象。 使用移動語義,開發人員可以聲明一個對象將不再使用,其值移至別處,通常伴隨有盡可能少的指針交換。

在某些情況下,開發人員需要明確指出這一點;但大多數情況下,編譯器可以利用移動感知對象並執行前所未有的超高效優化。 好消息是對您自己的類啟用移動語義非常簡單。 就像復制依賴於復制構造函數和復制賦值運算符一樣,移動語義依賴於移動構造函數和移動賦值運算符:


unique_handle(unique_handle && other) throw() :
m_value(other.release())
{
}

unique_handle & operator=(unique_handle && other) throw()
{
reset(other.release());
return *this;
}

rvalue 引用
C++ 2011 引入了一種新型引用,名為 rvalue 引用。 這種引用通過 && 聲明;前面代碼中在 unique_handle 成員中使用過。 雖然與現在名為 lvalue 引用的舊引用相似,但新的 rvalue 引用在初始化和重載解析方面展現出些許不同的規則。 這一話題先到此為止(稍后會有進一步討論)。 句柄至此有了移動語義,其主要優點是可以在 STL 容器中正確有效地存儲句柄。

錯誤處理
對 unique_handle 類模板的討論到此為止。 本月的最后一個主題是錯誤處理,這也是為后續專欄做的准備。 對異常和錯誤代碼利弊的爭論看似無休無止;但是,只要您想使用標准 C++ 庫,就必須習慣異常。 當然,Windows API 使用錯誤代碼,因此需要有所妥協。

我的錯誤處理方法是盡可能地少做錯誤處理,編寫異常安全的代碼但避免捕獲異常。 如果沒有異常處理程序,Windows 會自動生成一個錯誤報告,其中包含可以事后調試的小型崩潰轉儲。 僅在發生意外的運行時錯誤時引發異常,通過錯誤代碼來處理所有其他情況。 引發異常的原因不外乎代碼中的 bug 和計算機上降臨的災難。

我喜歡以訪問 Windows 注冊表為例。 如果無法寫入注冊表值,通常揭示程序中存在難以合理處理的較大問題。 這種情況應導致異常。 不過,無法讀取注冊表值應是可以遇見的情況,並妥善加以處理。 這種情況不應導致異常,而應返回一個布爾值或枚舉值,以指示是否無法讀取值以及發生此情況的原因。

Windows API 與其錯誤處理方式不甚一致;這是 API 多年演變的結果。 錯誤大都作為 BOOL 或 HRESULT 值返回。 對於某些其他錯誤,我習慣通過比較返回值與文檔記錄值來明確處理。

如果我的程序依賴某給定函數調用的成功才能繼續可靠工作,那么我使用圖 2 中所列的一個函數檢查返回值。

圖 2 檢查返回值


inline void check_bool(BOOL result)
{
if (!result)
{
throw check_failed(GetLastError());
}
}

inline void check_bool(bool result)
{
if (!result)
{
throw check_failed(GetLastError());
}
}

inline void check_hr(HRESULT result)
{
if (S_OK != result)
{
throw check_failed(result);
}
}

template <typename T>
void check(T expected, T actual)
{
if (expected != actual)
{
throw check_failed(0);
}
}

關於這些函數,有兩點需要指出。 第一點是 check_bool 函數經過重載,以便同時檢查句柄對象的有效性,它理應不允許隱式轉換為 BOOL。 第二點是 check_hr 函數,它顯式與 S_OK 進行比較,而不是使用更為常見的 SUCCEEDED 宏。 這可避免靜默接受開發人員向來不願看到的其他可疑成功代碼(如 S_FALSE)。

我初次嘗試編寫這些檢查函數時使用了一組重載。 但當我在各種不同項目中使用這些重載時,我意識到 Windows API 定義了太多的結果類型和宏,要創建一組適用於所有結果類型和宏的重載根本不可能。 這就用到了裝飾函數。 有幾次,我發現由於發生意外的重載解析而未能捕獲錯誤。 引發 check_failed 類型非常簡單:


struct check_failed
{
explicit check_failed(long result) :
error(result)
{
}

long error;
};

我可以給它裝飾各種花哨的功能,如添加錯誤消息支持,但有什么用呢? 我提供了錯誤值,以便對崩潰的應用程序進行檢查時可以輕松找出錯誤。 除此之外,別的功能只是徒增障礙而已。

有了這些檢查函數,我可以創建一個事件對象並為其設置信號,在出現錯誤時引發異常:


handle h(CreateEvent( ...
));

check_bool(h);

check_bool(SetEvent(h.get()));

異常處理
異常處理的另一個問題與效率有關。 開發人員對此又一次出現分歧,往往是因為他們有一些先入為主、不切實際的觀念。

異常處理的成本體現在兩個方面。 一方面是引發異常的成本。 引發異常往往比使用錯誤代碼慢,這也正是只應在出現致命錯誤時才應引發異常的原因之一。 如果一切順利,您根本不需要付出這樣的代價。

性能問題的另一方面,也是更常見的一方面原因是:萬一引發異常,那么為了確保調用正確的析構函數,會產生運行時開銷。 需要通過代碼來跟蹤需要執行哪些析構函數;這當然也會增加堆棧大小,這在大型代碼庫中對性能會有顯著影響。 請注意,無論是否實際引發異常,都需要付出此代價,因此盡量減少這種情況對確保良好的性能十分重要。

這意味着要確保編譯器對於什么函數可能引發異常擁有良好的判斷。 如果編譯器可以證明某些函數不會引發任何異常,它可以優化生成的代碼來定義和管理堆棧。 這就是我用異常規范裝飾整個句柄類模板和特征類成員函數的原因。 雖然在 C++ 2011 中已不再使用,但它是一項非常重要的平台特定優化功能。

本月內容到此結束。 您現在掌握了使用 Windows API 編寫可靠程序的關鍵要素之一。 下個月,請和我一起探索 Windows 線程池 API。

參考:http://keleyi.com/a/bjac/llj2p7ds.htm


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM