Windows API 為相容歷史套件,大多提供 ANSI (A 尾碼) 與 Unicode (W 尾碼) 兩個版本,例如 CreateFileA/CreateFileW、GetFileAttributesA/GetFileAttributesW。傳統封裝面臨兩難:
本文展示如何運用函數模板 + 特徵模板,實現「零重複、零轉換、編譯期分派」的優雅封裝。
#define TEMPLATE_BOOL_TRAIT_DEF1(trait, T, c) \
template <typename T> \
struct trait { static const bool value = c; };
#define TEMPLATE_BOOL_TRAIT_SPEC1(trait, sp, c) \
template <> \
struct trait<sp> { static const bool value = c; };
template <bool flag, typename T1, typename T2>
struct select_type;
template <typename T1, typename T2>
struct select_type<true, T1, T2> { typedef T1 type; };
template <typename T1, typename T2>
struct select_type<false, T1, T2> { typedef T2 type; };
關鍵技巧:利用函數模板參數推演避免顯式指定繁瑣的函數指標類型。
template <bool flag, typename T1, typename T2>
inline typename select_type<flag, T1, T2>::type
select_variable(T1 t1, T2 t2) {
return select_variable_impl(
typename select_type<flag, int, long>::type(),
t1, t2);
}
template <typename T1, typename T2>
inline T1 select_variable_impl(int, T1 t1, T2 t2) { return t1; }
template <typename T1, typename T2>
inline T2 select_variable_impl(long, T1 t1, T2 t2) { return t2; }
原理:flag=true 時 select_type 選 int,匹配第一個 select_variable_impl 多載;反之選 long 匹配第二個。編譯器自動推演 T1/T2 為函數指標類型。
TEMPLATE_BOOL_TRAIT_DEF1(is_ansi_char, T, false)
TEMPLATE_BOOL_TRAIT_SPEC1(is_ansi_char, char, true)
TEMPLATE_BOOL_TRAIT_SPEC1(is_ansi_char, char const, true)
TEMPLATE_BOOL_TRAIT_SPEC1(is_ansi_char, char volatile, true)
TEMPLATE_BOOL_TRAIT_SPEC1(is_ansi_char, char const volatile, true)
template <typename charT>
inline int IsDirectoryOrFile(const charT* path) {
DWORD dwFlag = select_variable<is_ansi_char<charT>::value>(
GetFileAttributesA, GetFileAttributesW)(path);
if (INVALID_FILE_ATTRIBUTES == dwFlag) return 0;
return (dwFlag & FILE_ATTRIBUTE_DIRECTORY) ? 1 : 2;
}
呼叫端可直接傳入 char* 或 wchar_t*,編譯期自動選擇正確 API。
template <typename T>
inline std::basic_string<T> GetExePath() {
T szExePath[MAX_PATH];
select_variable<is_ansi_char<T>::value>(
GetModuleFileNameA, GetModuleFileNameW)(NULL, szExePath);
return szExePath;
}
呼叫 GetExePath<char>() 得到 std::string,GetExePath<wchar_t>() 得到 std::wstring。
template <typename charT>
inline ULONGLONG GetDirSize(const charT* path) {
int ret = IsDirectoryOrFile(path);
if (0 == ret) return 0;
std::basic_string<charT> strPath = path;
if (1 == ret) {
if (strPath.length() - 1 != strPath.rfind((charT)'\\'))
strPath += (charT)'\\';
strPath += select_variable<is_ansi_char<charT>::value>("*.*", L"*.*");
}
ULONGLONG ullSumSize = 0;
// 關鍵:結構體類型也需根據字元型別選擇
typename select_type<is_ansi_char<charT>::value,
WIN32_FIND_DATAA, WIN32_FIND_DATAW>::type findData;
HANDLE hFindFile = select_variable<is_ansi_char<charT>::value>(
FindFirstFileA, FindFirstFileW)(strPath.c_str(), &findData);
for (BOOL bResult = (hFindFile != INVALID_HANDLE_VALUE);
bResult;
bResult = select_variable<is_ansi_char<charT>::value>(
FindNextFileA, FindNextFileW)(hFindFile, &findData)) {
if (findData.cFileName[0] == (charT)'.') continue;
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
strPath = strPath.substr(0, strPath.rfind((charT)'\\') + 1)
+ findData.cFileName;
ullSumSize += GetDirSize(strPath.c_str());
} else {
ullSumSize += (((ULONGLONG)findData.nFileSizeHigh) << 32)
+ findData.nFileSizeLow;
}
}
FindClose(hFindFile);
return ullSumSize;
}
| 維度 | 傳統巨集做法 | 模板特徵做法 |
|---|---|---|
| 程式碼重複 | 高 (A/W 兩套) | 零 (單一模板) |
| 執行期開銷 | 字串轉換成本 | 零 (編譯期分派) |
| 類型安全 | 弱 (巨集無類型檢查) | 強 (模板參數推演) |
| 擴充性 | 難 (新增 API 需改巨集) | 易 (只需新增特化) |
if constexpr 簡化 select_variable 邏輯ATL::CString、WTL 或 Windows Runtime Library (WRL) 成熟封裝透過特徵模板將「字元類型」與「API 版本選擇」綁定,透過函數模板參數推演消除函數指標類型的冗長宣告,這正是 C++ 模板元編程「零抽象成本」哲學的經典實踐。掌握此模式,不僅能優雅解決 Windows API 雙版本難題,更能舉一反三應用於任何「編譯期根據類型分派實作」的場景。