標籤:

C++模板元編程---編譯期類成員檢測

最近困擾了我幾天的問題終於解決了。

問題是這樣的

我現在定義一個模板類:

template<class T>nfoo{};n

我現在希望模板類 foo 實現以下功能:

當 T 類型中定義了類型 type 的時候,在 foo 內定義

using type = typename T::type;n

當 T 類型中沒有定義類型 type 時,在 foo 內定義

using type = T;n

總之,我們希望檢測T中到底有沒有定義類型 type, 並根據這個情況來確定把 foo 中的 type using 成 T 還是 typename::type。

對C++來說,想要完全不侵入性的在運行期檢測類中是否定義了某個類型或變數幾乎是不可能完成的事情。畢竟C++的類在沒有編譯期支持的情況下,沒有足夠的類的元數據,沒有反射機制,更不要說成員名最後編譯出來都只是地址和類型而已。但是通過藉助SFINAE的機制,我們可以在編譯期完成這件事。有興趣的可以看SFINAE - cppreference.com,這裡先不詳細展開這個機制,通過後面的實例,可以大致有個了解。

首先,創建一個模板類has_type用於檢測 T 中是否含有類型 type

template<class T, class U = void>nstruct ty_has_type {ntconst static int value = false;n};n

也就是說,通用情況下,我們認為類型T中是不包含類型 type 的,所以我們在主模板中將value定義為false。模板參數列表中的 U 用來檢測 T 中是否存在 type ,最初,我天真的寫出了這樣的代碼:

//錯誤的示範ntemplate<class T>nstruct ty_has_type<T, typename T::type>{ntconst static int value = true;n};n

並認為 T 中有 type 的時候,因為偏特化的版本描述更精確,所以會調用偏特化的版本。然而還是太拿衣服了,我們看看

struct foo{n using type = int;n};nnty_has_type<foo>n

會怎樣,首先,你只給模板穿了一個模板形參,那麼他會無條件的根據主模板的定義,U=void,將調用補全成為 ty_has_type<foo, void>,而在偏特化的版本中,模板參數實際上應該是 ty_has_type<foo, int> 。故這樣寫一輩子也不會調用偏特化的版本,除非foo類型中的type直接using成了void。那麼偏特化版本也會變成 ty_has_type<foo, void> ,那麼根據自動補全的 ty_has_type<foo, void>,就會調用偏特化的版本了。但是這肯定不是我們想要的東西,但我們發現了問題,偏特化版本的第二個模板參數,必須和U的默認模板參數一樣,即void,或者你選擇的其他任何類型。

那麼,怎麼辦呢?

有句名言:計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決,如果解決不了,那就增加兩層(後面兩句是我編的)。

我們寫一個輔助類,

template<class T>nstruct ty_sfinae_helper {ntusing type = void;n};n

然後將偏特化的版本改成這樣,

template<class T>nstruct ty_has_type<T, typename ty_sfinae_helper<typename T::type>::type>{ntconst static int value = true;n};n

OK,問題解決,經測試

template<class T>nstruct foo {ntusing type = T;n};nntemplate<class T>nstruct bar{n};nncout << ty_has_type<foo<int>>::value << endl;ncout << ty_has_type<bar<int>>::value << endl;n

結果是正確的。那麼前面說的SFINAE到底是什麼?

全稱 Substitution Failure Is Not an Error

即 匹配失敗不是錯誤。

可以看到上面的例子中,bar<int>內並沒有type類型定義,那麼在偏特化的版本中,去訪問T::type實際上是一個失敗的操作,然而在類型推導中,這並不是一個錯誤,這個推失敗了,編譯器就放棄這個偏特化版本,最後在能夠類型匹配的所有版本中尋找一個最精確的來具現化。這就是我們實現這個功能的原理

那麼,能再給力兒嗎?ty_has_type僅僅能夠檢測type,如果我想更換檢測的某個類型呢?

那就只能上宏了,自動生成類型。

template<class T, T val>nstruct constant_val {nttypedef constant_val<T, val> type;nttypedef T value_type;ntconstexpr operator value_type() const {nttreturn value;nt}ntconstexpr value_type operator()() const {nttreturn value;nt}ntstatic constexpr T value = val;n};nntypedef constant_val<bool, true> true_type;ntypedef constant_val<bool, false> false_typenn#define ty_cat(first, second) first##secondnntemplate<class T>nstruct ty_sfinae_wrapper {ntusing type = void;n};nn#define ty_has_xxx(member_name) ntemplate<class T, class U = void> nstruct ty_cat(has_, member_name){ ntusing type = false_type; ntconst static bool value = false_type::value; n};ntemplate<class T> nstruct ty_cat(has_, member_name)<T, typename ty_sfinae_wrapper<typename T::member_name>::type>{ntusing type = true_type;ntconst static bool value = true_type::value;n};n

以上是全部代碼,其實大同小異,除了我把 true 和 false 外包給了一個編譯期常量的類型以外,沒什麼區別。

根據你希望檢測的類型name,在任何允許類定義的地方,寫上

ty_has_xxx(name) ,然後在後面直接使用類型 has_name<某個類型>::value就可以了。

例如我希望檢測是否含有類型 iterator。可以寫出以下代碼:

ty_has_xxx(iterator);nint main(){n cout << has_iterator<vector<int>>::value << endl;n}n

輸出 1

這個特性也被加在了C++模板元編程---lambda表達式簡單實現中,這允許我們對於內部沒有定義 type 的類型,寫出諸如如下的代碼:

lambda<vector<_1>>::type::apply<double>::type dvec;n

dvec會是一個 vector<double>,而不會報錯 vector<double> 中沒有類型 type。具體可移步

另,如果希望檢測類中是否具有可訪問的某個成員,無論是成員變數,成員函數,或者類型,只要可訪問的話,可以在宏定義中再接上以下代碼

template<class T> nstruct ty_cat(has_, member_name)<T, typename ty_sfinae_wrapper<decltype(T::member_name)>::type>{ntusing type = true_type;ntconst static bool value = true_type::value;n};n


推薦閱讀:

萌新刷題(一)A + B 問題
刷題大戰 09 代碼評審報告
std::make_index_sequence的簡單實現和簡單應用
GacUI:初步完成Workflow腳本轉C++的工作
《C++ Primer》讀書筆記-第七章 06 類的靜態成員

TAG:CC | C | 模板C |