enable_if

1  導言


使用 enable_if 系列模板可以控制一個函數模板或類模板偏特化是否包含在基於模板參數屬性的一系列匹配函數或偏特化中。比如,我們可以定義一個只對某些類型(通過特徵類[traits class]定義)有效——當然也只匹配這些類型——的函數模板。enable_if 也可以對類模板偏特化實現同樣的效果。enable_if 的應用在 [1] 和 [2] 中有詳細的介紹。


1.1  大綱

namespace boost {
  template <class Cond, class T = void> struct enable_if;
  template <class Cond, class T = void> struct disable_if;
  template <class Cond, class T> struct lazy_enable_if;
  template <class Cond, class T> struct lazy_disable_if;

  template <bool B, class T = void> struct enable_if_c;
  template <bool B, class T = void> struct disable_if_c;
  template <bool B, class T> struct lazy_enable_if_c;
  template <bool B, class T> struct lazy_disable_if_c;
}

1.2  背景

C++ 中“類型敏感的”模板函數重載依賴於 SFINAE (substitution-failure-is-not-an-error) 原則 [3]:在函數模板的實例化過程中,如果形成的某個參數或返回值類型無效那麼這個實例將從重載決議集中去掉而不是引發一個編譯錯誤。下面這個例子,出自 [1],演示了這個原則的重要性。
int negate(int i) { return -i; }

template <class F>
typename F::result_type negate(const F& f) { return -f(); }

我們假設編譯器遇到了 negate(1) 的調用。很明顯第一個定義是個好選擇,但是編譯器必須在檢查所有的定義後才能作出決定,這個檢查過程包含對模板的實例化。使用int 作爲類型 F 對第二個定義進行實例化將產生:
int::result_type negate(const int&);

這裏的返回值類型是無效的。 如果把這種情況看作是一種錯誤,那麼添加一個無關的函數模板(從來不會被調用)也將導致原本有效的代碼無法通過編譯。由於 SFINAE 原則的存在,上面的例子不會產生編譯錯誤,當然也不是良好的編程風格。在這種情況下編譯器會簡單地從重載決議集中拋棄後一個negate 的定義。

enable_if 模板就是一個控制是否形成符合 SFINAE  條件的工具。

2  enable_if模板

enable_if 面板的名稱分爲三部分:一個可選的 lazy_ 標記、enable_ifdisable_if和一個可選的 _c 標記。這三部分的 8 種組合都存在於該庫中。 lazy_ 標記的作用在3.3 中討論。名稱的第二部分表示使用值爲 true 的參數啓用當前重載還是禁用當前重載。名稱的第三部分表示參數是一個 bool 值(有 _c 後綴)還是一個包含名稱爲 value 的靜態 bool 值的類型。後者與 Boost.MPL 配合使用。

enable_if_cenable_if 的定義如下:(下面我們只寫 enable_if,其實他們是被包含在 boost 命名空間中的。)
template <bool B, class T = void>
struct enable_if_c {
  typedef T type;
};

template <class T>
struct enable_if_c<false, T> {};

template <class Cond, class T = void>
struct enable_if : public enable_if_c<Cond::value, T> {};

如果參數 B 爲 true, enable_if_c 模板的實例包含一個成員類型 type,被定義爲類型 T。如果 B 爲 false 則不會定義這樣的成員類型。所以enable_if_c<B, T>::type 可以是一個有效的或者無效的類型表達式,這取決於 B 的值。當有效時,enable_if_c<B, T>::type 等價於T因此 enable_if_c 模板可以用來控制函數何時參與重載決議何時不參與。比如,下面這個函數對所有的算術類型(根據 Boost type_traits library 的分類)有效:

template <class T>
typename enable_if_c<boost::is_arithmetic<T>::value, T>::type 
foo(T t) { return t; }

disable_if_cenable_if_c 的功能一樣,只是參數的含義相反。下面着函數對所有的“非算術類型”有效:
template <class T>
typename disable_if_c<boost::is_arithmetic<T>::value, T>::type 
bar(T t) { return t; }

爲了和 Boost.MPL 一起使用,我們提供了接收任何包含名爲 value 的 bool 型常量成員的類型作爲參數的 enable_if 模板。MPLbool_、 and_or_、 和 not_ 都可以用來構建這種類型。Boost.Type_traits 庫中的特徵類也符合這個慣例。比如,上面例子中的 foo 函數也可以這樣寫:
template <class T>
typename enable_if<boost::is_arithmetic<T>, T>::type 
foo(T t) { return t; }

3  使用 enable_if

enable_if 模板在 boost/utility/enable_if.hpp 中定義,這個文件被boost/utility.hpp 所包含

enable_if 既可以作爲返回值也可以作爲一個額外的參數。比如,上一節中的函數 foo 也可以這麼些:
template <class T>
T foo(T t, typename enable_if<boost::is_arithmetic<T> >::type* dummy = 0); 

我們給這個函數添加了一個額外的 void* 類型的參數但是爲它指定了一個默認值,所以這個參數對調用該函數的客戶代碼是不可見的。注意 enable_if 的第二個模板參數沒有給出,因爲默認的void 正好滿足需要。

把控制條件作爲一個參數或是返回值很大程度上取決於編程風格,但是對於某些函數來說只有一個可選項:
  • 運算符的參數個數是固定的,所以 enable_if 只能用作返回值。
  • 構造函數和析構函數沒有返回值,所以只能添加一個額外的參數。
  • 對於類型轉換運算符,好像還沒有辦法使用此機制。但是類型轉換構造函數可以使用添加一個額外的參數的方法來使用此機制。

3.1 模板類偏特化的啓用與禁用

類模板偏特化可以使用 enable_if 來控制其啓用與禁用。爲達到這個目的,需要爲模板添加一個額外的模板參數用於控制啓用與禁用。這個參數的默認值是 void。比如:
template <class T, class Enable = void> 
class A { ... };

template <class T>
class A<T, typename enable_if<is_integral<T> >::type> { ... };

template <class T>
class A<T, typename enable_if<is_float<T> >::type> { ... };

使用任何整數類型實例化的 A 與第一個偏特化匹配,任何浮點類型與二個相匹配。任何其他類型匹配主模板。條件可以是任何依賴模板參數的編譯期邏輯型表達式。同樣需要注意的是,enable_if 的第二個參數不需要給出;默認值(void)在這裏可以勝任。

3.2  重疊的控制條件

一旦編譯器在檢查控制條件後把這個函數包含進重載決議集中,那麼它就會進一步使用普通的 C++ 重載決議規則來選擇最匹配的函數。具體來說,各個控制條件之間沒有順序之分,使用了控制條件的各個函數之間如果不能相互排斥,將導致二義性。比如:
template <class T>
typename enable_if<boost::is_integral<T>, void>::type 
foo(T t) {}

template <class T>
typename enable_if<boost::is_arithmetic<T>, void>::type 
foo(T t) {}

所有的整數類型也是算數類型。所以,對於 foo(1) 這樣的調用,兩個條件都是 true,於是兩個都將包含進重載決議集中。它們都可以很好地被匹配,於是二義性就產生了。當然,如果通過其他的參數可以解決二義性,一個或多個控制條件同時爲 true 是可以的。

以上討論同樣適用於在類模板片特化中使用 enable_if

3.3  惰性(lazy) enable_if

在某些情況下,在某個控制條件不爲 true 時避免實例化一個函數簽名的某個部分是很有用的。比如:
template <class T, class U> class mult_traits;

template <class T, class U>
typename enable_if<is_multipliable<T, U>, typename mult_traits<T, U>::type>::type
operator*(const T& t, const U& u) { ... }

假設類模板 mult_traits 是一個定義一個乘法運算符結果類型的特徵類,is_multipliable 是一個定義可以用於該運算符的類型的特徵類。只要對於某些類型 A 和 B,is_multipliable<A, B>::valuetrue 那麼 mult_traits<A, B>::type 就會被定義出來。

現在,試着用類型 C 和 D 調用運算符 * (還有其他一些重載),其中 is_multipliable<C, D>::value falsemult_traits<C, D>::type 未定義。這種情況在某些編譯器上是一種錯誤。這時,SFINAE 不起作用因爲無效的類型是另外一個模板的參數。在這種情況下可以使用 lazy_enable_iflazy_disable_if (還有它們的 _c 版本):
template<class T, class U>
typename lazy_enable_if<is_multipliable<T, U>, mult_traits<T, U> >::type
operator*(const T& t, const U& u) { ... }

lazy_enable_if 的第二個參數必須是一個在第一個參數(條件)爲 true 時定義了一個名字爲 type 的的內嵌類型。

注意
使用了一個特徵類的一個成員類型或靜態常量將導致這個特化的所有成員(類型和靜態常量)被實例化。因此,如果你的特徵類在某些情況下包含無效類型,那麼你應該使用兩個不同的模板來描述條件和類型映射(conditions and the type mappings)。在上面的例子中,is_multipliable<T, U>::value 定義了何時 whenmult_traits<T, U>::type 有效。

3.4  編譯器變通解決方案

如果在控制者(enabler)中唯一用於解決二義性的因子是另外一個條件,那麼某些編譯器標誌會導致二義性。比如,某些編譯器(例如 GCC 3.2)將會把下面兩個函數診斷爲具有二義性:
template <class T>
typename enable_if<boost::is_arithmetic<T>, T>::type 
foo(T t);

template <class T>
typename disable_if<boost::is_arithmetic<T>, T>::type 
foo(T t);

有兩個臨時解決方案:
  • 使用一個額外的參數用於解決二義性。使用默認值使它對調用者不可見。比如:
    template <int> struct dummy { dummy(int) {} };
    
    template <class T>
    typename enable_if<boost::is_arithmetic<T>, T>::type 
    foo(T t, dummy<0> = 0);
    
    template <class T>
    typename disable_if<boost::is_arithmetic<T>, T>::type 
    foo(T t, dummy<1> = 0);
    


  • 在不同的命名空間中定義這兩個函數,然後使用 using 把它們帶到同一個命名空間中:
    namespace A {
      template <class T>
      typename enable_if<boost::is_arithmetic<T>, T>::type 
      foo(T t);
    }
    
    namespace B {
      template <class T>
      typename disable_if<boost::is_arithmetic<T>, T>::type 
      foo(T t);
    }
    
    using A::foo;
    using B::foo;
    
    
    注意,第二個解決方案不能用在成員模板中。另一方面,運算符不能接受額外的參數,所以第一個解決方案不可使用。總結一下:這兩個解決方案都不能用於需要定義爲成員方法的模板化的運算符(賦值和下標運算符)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章