C ++開(kāi)發(fā)人員中有一部分人喜歡模板元編程(TMP)。
還有其他所有C ++開(kāi)發(fā)人員。
雖然我認(rèn)為自己傾向于狂熱者陣營(yíng)。但是我遇到過(guò)的人,相比于愛(ài)好者來(lái)說(shuō),更多的人對(duì)它沒(méi)有什么興趣甚至感到厭惡。你是哪個(gè)陣營(yíng)的?
在我看來(lái),TMP之所以無(wú)法為許多人接受的原因之一是它通常很晦澀。 有時(shí)它看起來(lái)像是黑魔法,只保留給可以理解其方言的開(kāi)發(fā)人員的一個(gè)非常特殊的亞種。 當(dāng)然,有時(shí)我們會(huì)遇到偶爾可以理解的TMP,但是平均而言,我發(fā)現(xiàn)它比常規(guī)代碼更難理解。
我想指出的是,TMP不必一定是這種方式。
我將向你展示如何使TMP代碼更具表現(xiàn)力。 它并沒(méi)有那么難。
TMP通常被描述為C ++語(yǔ)言中的一種語(yǔ)言。 因此,為了使TMP更具表現(xiàn)力,我們只需要應(yīng)用與常規(guī)代碼相同的規(guī)則即可。 為了說(shuō)明這一點(diǎn),我們將采用一段只有我們最勇敢的人才能理解的代碼,并在其上應(yīng)用以下兩個(gè)表達(dá)性準(zhǔn)則:
- 選擇好名字,
- 并分離出抽象層次。
我給你說(shuō)過(guò)了,它并沒(méi)有那么難。
示例代碼的目的
我們將編寫一個(gè)API,以檢查表達(dá)式對(duì)于給定類型是否有效。
例如,給定類型T,我們想知道T是否可遞增,也就是說(shuō),對(duì)于類型T的對(duì)象t,如下表達(dá)式是否合法:
++t
如果T為int,則表達(dá)式有效;如果T為std :: string,則表達(dá)式無(wú)效。
這是實(shí)現(xiàn)它的TMP的典型部分:
template< typename, typename = void >
struct is_incrementable : std::false_type { };
template< typename T >
struct is_incrementable<T,
std::void_t<decltype( ++std::declval<T&>() )>
> : std::true_type { };
我不知道你需要多少時(shí)間來(lái)解析此代碼,但是花了我大量的時(shí)間才能全部解決。 讓我們看看如何重新編寫此代碼,以使其更易于理解。
公平地說(shuō),我必須說(shuō),要了解TMP,你需要了解一些結(jié)構(gòu)。 有點(diǎn)像需要了解“ if”,“ for”和函數(shù)重載以了解C ++的知識(shí),TMP具有一些先決條件,例如“ std :: true_type”和SFINAE。 但是,如果你不認(rèn)識(shí)它們,請(qǐng)不要擔(dān)心,我將一路向你解釋。
基礎(chǔ)知識(shí)
如果您已經(jīng)熟悉TMP,則可以跳到下一部分。
我們的目標(biāo)是能夠以這種方式查詢類型:
is_incrementable<T>::value
is_incrementable <T> 是一種類型,具有一個(gè)公共布爾成員value,如果T是可遞增的(例如T為int),則為true;否則,則為false(例如T為std :: string)。
我們將使用std :: true_type。 它是僅具有等于true的公共布爾成員值的類型。 在T可以遞增的情況下,我們將從它繼承 is_incrementable <T>。 而且,你已經(jīng)猜到了,如果T不能遞增,則從std :: false_type繼承。
為了允許有兩個(gè)可能的定義,我們使用模板特化。 一種專門繼承自std :: true_type,另一種專門繼承自std :: false_type。 因此,我們的解決方案將大致如下所示:
template<typename T>
struct is_incrementable : std::false_type{};
template<typename T>
struct is_incrementable<something that says that T is incrementable> : std::true_type{};
特化基于SFINAE。 簡(jiǎn)而言之,我們將編寫一些代碼,嘗試在特化中自增T。 如果T確實(shí)是可遞增的,則此代碼將有效,特化就會(huì)實(shí)例化(因?yàn)樗冀K優(yōu)先于主模板)。它會(huì)繼承std :: true_type。
另一方面,如果T不可遞增,則特化將無(wú)效。 在這種情況下,SFINAE表示無(wú)效的實(shí)例化不會(huì)停止編譯。 它只是被完全丟棄,剩下的唯一模板是主模板,即從std :: false_type繼承。
選一個(gè)好名字
文章頂部的代碼使用了std :: void_t。 此結(jié)構(gòu)出現(xiàn)在C ++ 17的標(biāo)準(zhǔn)中,但可以立即在C ++ 11中復(fù)制:
template<typename...>
using void_t = void;
void_t只是實(shí)例化它傳遞的模板類型,并且從不使用它們。 如果可以的話,它就像模板的代孕母親。
為了使代碼正常工作,我們以這種方式編寫特化代碼:
template<typename T>
struct is_incrementable<T, void_t<decltype(++std::declval<T&>())>> : std::true_type{};
好吧,要了解TMP,還需要了解decltype和declval:decltype返回其參數(shù)的類型,而declval <T>()的作用就像在decltype表達(dá)式中實(shí)例化了T類型的對(duì)象一樣(這很有用,因?yàn)槲覀儾恍枰欢ㄖ繲的構(gòu)造函數(shù)是什么樣的)。所以decltype(++ std :: declval <T&>())是在T上調(diào)用的operator ++的返回類型。
如上所述,void_t只是實(shí)例化此返回類型的助手。它不攜帶任何數(shù)據(jù)或行為,只是一種啟動(dòng)板,用于實(shí)例化由decltype返回的類型。
如果增量表達(dá)式無(wú)效,則由void_t進(jìn)行的實(shí)例化將失敗,SFINAE啟動(dòng),is_incrementable 解析為繼承自std :: false_type的主模板。
這是一個(gè)很棒的機(jī)制,但我對(duì)這個(gè)名字有 異議。在我看來(lái),這絕對(duì)是錯(cuò)誤的抽象級(jí)別:將其實(shí)現(xiàn)為void,但是要做的是嘗試實(shí)例化一個(gè)類型。通過(guò)將這些信息處理為代碼,TMP表達(dá)式立即清晰起來(lái):
template<typename...>
using try_to_instantiate = void;
template<typename T>
struct is_incrementable<T, try_to_instantiate<decltype(++std::declval<T&>())>> : std::true_type{};
考慮到使用兩個(gè)模板參數(shù)的特化,主模板也必須具有兩個(gè)參數(shù)。 為了避免用戶傳值,我們提供了一個(gè)默認(rèn)類型,即void。 現(xiàn)在的問(wèn)題是如何命名該技術(shù)參數(shù)?
解決此問(wèn)題的一種方法是完全不命名(頂部的代碼使用了此選項(xiàng)):
template<typename T, typename = void>
struct is_incrementable : std::false_type{};
我認(rèn)為這是一種說(shuō)“別看這個(gè),不相關(guān),而且只出于技術(shù)原因”的一種方式。 另一種選擇是給它起一個(gè)名字,說(shuō)明它的意思。 第二個(gè)參數(shù)是嘗試實(shí)例化特化形式中的表達(dá)式,因此我們可以將此信息寫到名稱中,從而提供到目前為止的完整解決方案:
template<typename...>
using try_to_instantiate = void;
template<typename T, typename Attempt = void>
struct is_incrementable : std::false_type{};
template<typename T>
struct is_incrementable<T, try_to_instantiate<decltype(++std::declval<T&>())>> : std::true_type{};
分離抽象層次
我們可以在這里就完成了。 但是可以說(shuō) is_incrementable 中的代碼仍然過(guò)于技術(shù)化,可能會(huì)被推到較低的抽象層。 此外,可以想象,在某個(gè)時(shí)候我們將需要使用相同的技術(shù)來(lái)檢查其他表達(dá)式,并且最好將檢查機(jī)制排除在外,以避免代碼重復(fù)。
我們最終將得到類似于is_detected功能的內(nèi)容。
上面代碼中變化最大的部分顯然是decltype表達(dá)式。 因此,讓我們將其作為模板參數(shù)放到輸入中。 但是,再次讓我們仔細(xì)選擇名稱:此參數(shù)表示一個(gè)表達(dá)式類型。
此表達(dá)式本身取決于模板參數(shù)。 因此,我們不只是使用類型名作為參數(shù),而是使用模板(因此使用template <typename>類):
template<typename T, template<typename> class Expression, typename Attempt = void>
struct is_detected : std::false_type{};
template<typename T, template<typename> class Expression>
struct is_detected<T, Expression, try_to_instantiate<Expression<T>>> : std::true_type{};
然后 is_incrementable 就變成了
template<typename T>
using increment_expression = decltype(++std::declval<T&>());
template<typename T>
using is_incrementable = is_detected<T, increment_expression>;
在表達(dá)式中允許幾種類型
到目前為止,我們已經(jīng)使用了僅涉及一種類型的表達(dá)式,但是能夠?qū)⒍喾N類型傳遞給表達(dá)式將是很好的選擇。 例如,用于測(cè)試兩種類型是否可相互賦值。
為此,我們需要使用可變參數(shù)模板來(lái)表示表達(dá)式中的類型。 我們想像下面的代碼一樣添加一些點(diǎn),但是它不起作用:
template<typename... Ts, template<typename...> class Expression, typename Attempt = void>
struct is_detected : std::false_type{};
template<typename... Ts, template<typename...> class Expression>
struct is_detected<Ts..., Expression, try_to_instantiate<Expression<Ts...>>> : std::true_type{};
這是行不通的,因?yàn)榭勺儏?shù)包的類型名稱... Ts將占用所有模板參數(shù),因此需要將其放在最后。 但是默認(rèn)模板參數(shù)Attempt也需要放在最后。 所以我們遇到一個(gè)問(wèn)題。
首先,將包移至模板參數(shù)列表的末尾,然后刪除“Attempt”的默認(rèn)類型:
template<template<typename...> class Expression, typename Attempt, typename... Ts>
struct is_detected : std::false_type{};
template<template<typename...> class Expression, typename... Ts>
struct is_detected<Expression, try_to_instantiate<Expression<Ts...>>, Ts...> : std::true_type{};
但是傳遞給Attempt什么類型呢?
第一反應(yīng)可能是傳void,因?yàn)閠ry_to_instantiate的成功分支處理了void,因此我們需要傳遞它以實(shí)例化特化模板。
但是我認(rèn)為這樣做會(huì)使調(diào)用者撓頭:傳void意味著什么? 與函數(shù)的返回類型相反,void在TMP中并不表示“無(wú)”,因?yàn)関oid是一種類型。
因此,給它起一個(gè)更好地表達(dá)我們意圖的名稱。 有人稱這種事情為“dummy”,但我喜歡給個(gè)更準(zhǔn)確的名稱:
using disregard_this = void;
但是我猜這個(gè)名稱因人而異。
然后可以通過(guò)以下方式編寫賦值檢查:
template<typename T, typename U>
using assign_expression = decltype(std::declval<T&>() = std::declval<U&>());
template<typename T, typename U>
using are_assignable = is_detected<assign_expression, disregard_this, T, U>
當(dāng)然,即使disregard_this通過(guò)說(shuō)我們不需要擔(dān)心來(lái)讓讀者放心,但它仍然會(huì)放在那礙事。
一種解決方案是將其隱藏在間接級(jí)別之后:is_detected_impl。 “ impl_”通常在TMP中(以及在其他地方)也意味著“間接級(jí)別”。 雖然我覺(jué)得這個(gè)詞不自然,但我想不出一個(gè)更好的名字,而且由于很多TMP代碼都使用它,所以這個(gè)名字也約定俗成了。
我們還將利用這種間接級(jí)別來(lái)獲取:: value屬性,從而避免所有元素在每次使用它時(shí)都要調(diào)用一次。
最終的代碼如下:
template<typename...>
using try_to_instantiate = void;
using disregard_this = void;
template<template<typename...> class Expression, typename Attempt, typename... Ts>
struct is_detected_impl : std::false_type{};
template<template<typename...> class Expression, typename... Ts>
struct is_detected_impl<Expression, try_to_instantiate<Expression<Ts...>>, Ts...> : std::true_type{};
template<template<typename...> class Expression, typename... Ts>
constexpr bool is_detected = is_detected_impl<Expression, disregard_this, Ts...>::value;
這是如何使用它:
template<typename T, typename U>
using assign_expression = decltype(std::declval<T&>() = std::declval<U&>());
template<typename T, typename U>
constexpr bool is_assignable = is_detected<assign_expression, T, U>;
生成的值可以在編譯時(shí)或運(yùn)行時(shí)使用。 以下程序:
// 編譯時(shí)使用
static_assert(is_assignable<int, double>, "");
static_assert(!is_assignable<int, std::string>, "");
// 運(yùn)行時(shí)使用
std::cout << std::boolalpha;
std::cout << is_assignable<int, double> << '\n';
std::cout << is_assignable<int, std::string> << '\n';
編譯成功,而且輸出:
true
false
TMP不必那么復(fù)雜
誠(chéng)然,要了解TMP,需要滿足一些先決條件,例如SFINAE等。 但是除此之外,沒(méi)有必要把使用TMP的代碼搞的比實(shí)際需要的復(fù)雜。
考慮一下現(xiàn)在進(jìn)行單元測(cè)試的好習(xí)慣:不能因?yàn)椴皇巧a(chǎn)代碼,所以我們就降低質(zhì)量標(biāo)準(zhǔn)。 嗯,對(duì)于TMP來(lái)說(shuō)更是如此:這是生產(chǎn)代碼。 因此,讓我們將其與代碼的其余部分一樣對(duì)待,并盡最大努力使其表現(xiàn)力更好。 很有可能會(huì)吸引更多的人。 社區(qū)越豐富,好主意就越多。