Fluent C++:富有表現力的C ++模板元編程

原文

C ++開發人員中有一部分人喜歡模板元編程(TMP)。

還有其他所有C ++開發人員。

雖然我認為自己傾向于狂熱者陣營。但是我遇到過的人,相比于愛好者來說,更多的人對它沒有什么興趣甚至感到厭惡。你是哪個陣營的?

在我看來,TMP之所以無法為許多人接受的原因之一是它通常很晦澀。 有時它看起來像是黑魔法,只保留給可以理解其方言的開發人員的一個非常特殊的亞種。 當然,有時我們會遇到偶爾可以理解的TMP,但是平均而言,我發現它比常規代碼更難理解。

我想指出的是,TMP不必一定是這種方式。

我將向你展示如何使TMP代碼更具表現力。 它并沒有那么難。

TMP通常被描述為C ++語言中的一種語言。 因此,為了使TMP更具表現力,我們只需要應用與常規代碼相同的規則即可。 為了說明這一點,我們將采用一段只有我們最勇敢的人才能理解的代碼,并在其上應用以下兩個表達性準則:

  • 選擇好名字,
  • 并分離出抽象層次。

我給你說過了,它并沒有那么難。

示例代碼的目的

我們將編寫一個API,以檢查表達式對于給定類型是否有效。

例如,給定類型T,我們想知道T是否可遞增,也就是說,對于類型T的對象t,如下表達式是否合法:

++t

如果T為int,則表達式有效;如果T為std :: string,則表達式無效。

這是實現它的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 { };

我不知道你需要多少時間來解析此代碼,但是花了我大量的時間才能全部解決。 讓我們看看如何重新編寫此代碼,以使其更易于理解。

公平地說,我必須說,要了解TMP,你需要了解一些結構。 有點像需要了解“ if”,“ for”和函數重載以了解C ++的知識,TMP具有一些先決條件,例如“ std :: true_type”和SFINAE。 但是,如果你不認識它們,請不要擔心,我將一路向你解釋。

基礎知識

如果您已經熟悉TMP,則可以跳到下一部分。

我們的目標是能夠以這種方式查詢類型:

is_incrementable<T>::value

is_incrementable <T> 是一種類型,具有一個公共布爾成員value,如果T是可遞增的(例如T為int),則為true;否則,則為false(例如T為std :: string)。

我們將使用std :: true_type。 它是僅具有等于true的公共布爾成員值的類型。 在T可以遞增的情況下,我們將從它繼承 is_incrementable <T>。 而且,你已經猜到了,如果T不能遞增,則從std :: false_type繼承。

為了允許有兩個可能的定義,我們使用模板特化。 一種專門繼承自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。 簡而言之,我們將編寫一些代碼,嘗試在特化中自增T。 如果T確實是可遞增的,則此代碼將有效,特化就會實例化(因為它始終優先于主模板)。它會繼承std :: true_type。

另一方面,如果T不可遞增,則特化將無效。 在這種情況下,SFINAE表示無效的實例化不會停止編譯。 它只是被完全丟棄,剩下的唯一模板是主模板,即從std :: false_type繼承。

選一個好名字

文章頂部的代碼使用了std :: void_t。 此結構出現在C ++ 17的標準中,但可以立即在C ++ 11中復制:

template<typename...>
using void_t = void;

void_t只是實例化它傳遞的模板類型,并且從不使用它們。 如果可以的話,它就像模板的代孕母親。

為了使代碼正常工作,我們以這種方式編寫特化代碼:

template<typename T>
struct is_incrementable<T, void_t<decltype(++std::declval<T&>())>> : std::true_type{};

好吧,要了解TMP,還需要了解decltype和declval:decltype返回其參數的類型,而declval <T>()的作用就像在decltype表達式中實例化了T類型的對象一樣(這很有用,因為我們不需要一定知道T的構造函數是什么樣的)。所以decltype(++ std :: declval <T&>())是在T上調用的operator ++的返回類型。

如上所述,void_t只是實例化此返回類型的助手。它不攜帶任何數據或行為,只是一種啟動板,用于實例化由decltype返回的類型。

如果增量表達式無效,則由void_t進行的實例化將失敗,SFINAE啟動,is_incrementable 解析為繼承自std :: false_type的主模板。

這是一個很棒的機制,但我對這個名字有 異議。在我看來,這絕對是錯誤的抽象級別:將其實現為void,但是要做的是嘗試實例化一個類型。通過將這些信息處理為代碼,TMP表達式立即清晰起來:

template<typename...>
using try_to_instantiate = void;

template<typename T>
struct is_incrementable<T, try_to_instantiate<decltype(++std::declval<T&>())>> : std::true_type{};

考慮到使用兩個模板參數的特化,主模板也必須具有兩個參數。 為了避免用戶傳值,我們提供了一個默認類型,即void。 現在的問題是如何命名該技術參數?

解決此問題的一種方法是完全不命名(頂部的代碼使用了此選項):

template<typename T, typename = void>
struct is_incrementable : std::false_type{};

我認為這是一種說“別看這個,不相關,而且只出于技術原因”的一種方式。 另一種選擇是給它起一個名字,說明它的意思。 第二個參數是嘗試實例化特化形式中的表達式,因此我們可以將此信息寫到名稱中,從而提供到目前為止的完整解決方案:

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{};

分離抽象層次

我們可以在這里就完成了。 但是可以說 is_incrementable 中的代碼仍然過于技術化,可能會被推到較低的抽象層。 此外,可以想象,在某個時候我們將需要使用相同的技術來檢查其他表達式,并且最好將檢查機制排除在外,以避免代碼重復。

我們最終將得到類似于is_detected功能的內容。

上面代碼中變化最大的部分顯然是decltype表達式。 因此,讓我們將其作為模板參數放到輸入中。 但是,再次讓我們仔細選擇名稱:此參數表示一個表達式類型。

此表達式本身取決于模板參數。 因此,我們不只是使用類型名作為參數,而是使用模板(因此使用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>;

在表達式中允許幾種類型

到目前為止,我們已經使用了僅涉及一種類型的表達式,但是能夠將多種類型傳遞給表達式將是很好的選擇。 例如,用于測試兩種類型是否可相互賦值。

為此,我們需要使用可變參數模板來表示表達式中的類型。 我們想像下面的代碼一樣添加一些點,但是它不起作用:

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{};

這是行不通的,因為可變參數包的類型名稱... Ts將占用所有模板參數,因此需要將其放在最后。 但是默認模板參數Attempt也需要放在最后。 所以我們遇到一個問題。

首先,將包移至模板參數列表的末尾,然后刪除“Attempt”的默認類型:

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什么類型呢?

第一反應可能是傳void,因為try_to_instantiate的成功分支處理了void,因此我們需要傳遞它以實例化特化模板。

但是我認為這樣做會使調用者撓頭:傳void意味著什么? 與函數的返回類型相反,void在TMP中并不表示“無”,因為void是一種類型。

因此,給它起一個更好地表達我們意圖的名稱。 有人稱這種事情為“dummy”,但我喜歡給個更準確的名稱:

using disregard_this = void;

但是我猜這個名稱因人而異。

然后可以通過以下方式編寫賦值檢查:

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>

當然,即使disregard_this通過說我們不需要擔心來讓讀者放心,但它仍然會放在那礙事。

一種解決方案是將其隱藏在間接級別之后:is_detected_impl。 “ impl_”通常在TMP中(以及在其他地方)也意味著“間接級別”。 雖然我覺得這個詞不自然,但我想不出一個更好的名字,而且由于很多TMP代碼都使用它,所以這個名字也約定俗成了。

我們還將利用這種間接級別來獲取:: value屬性,從而避免所有元素在每次使用它時都要調用一次。

最終的代碼如下:

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>;

生成的值可以在編譯時或運行時使用。 以下程序:

// 編譯時使用
static_assert(is_assignable<int, double>, "");
static_assert(!is_assignable<int, std::string>, "");

// 運行時使用
std::cout << std::boolalpha;
std::cout << is_assignable<int, double> << '\n';
std::cout << is_assignable<int, std::string> << '\n';

編譯成功,而且輸出:

true
false

TMP不必那么復雜

誠然,要了解TMP,需要滿足一些先決條件,例如SFINAE等。 但是除此之外,沒有必要把使用TMP的代碼搞的比實際需要的復雜。

考慮一下現在進行單元測試的好習慣:不能因為不是生產代碼,所以我們就降低質量標準。 嗯,對于TMP來說更是如此:這是生產代碼。 因此,讓我們將其與代碼的其余部分一樣對待,并盡最大努力使其表現力更好。 很有可能會吸引更多的人。 社區越豐富,好主意就越多。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容