Fluent C++:富有表現(xiàn)力的C ++模板元編程

原文

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ū)越豐富,好主意就越多。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容