天天看點

C++(标準庫):08---Type Trait和Type Utility(type_traits庫)

一、前言

  • C++标準庫幾乎每樣東西都以template為根基。為了更多地支援template程式設計,彼岸準哭提供了template通用工具,協助應用程式開發人員和程式庫作者
  • Type Trait,由TR1引入,在C++11中被大幅度擴充,定義出因type而異的行為。它們可被用來針對type優化代碼,以便提供特别能力
  • 其他工具如reference和function wrapper,也為程式設計帶來若幹幫助

二、Type Trait的目的

  • 目的:提供一種用來處理type屬性的方法。它是個template,可在編譯期根據一個或多個template實參産出一個type或value

示範案例1(std::is_pointer<>)

#include <type_traits>

template<typename T>
void foo(const T& val)
{
    if (std::is_pointer<T>::value) {
        std::cout << "foo() called for a pointer" << std::endl;
    }
    else {
        std::cout << "foo() called for a value" << std::endl;
    }
}
int main()
{
    int num;
    int *p = &num;
    
    foo(num);
    foo(p);
}      
  • 運作結果如下:
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • 代碼解釋:
  • 這裡的std::is_pointer<>屬于一個traits類。如果傳入的參數類型為指針類型,其傳回std::true_type;如果傳入的參數類型不是指針類型,其傳回std::false_type。然後std::true_type::value或std::false_type::value傳回相對應的true或false
  • 第一個foo()調用傳入的參數為非指針類型

示範案例2(std::true_type、std::false_type)

  • 我們修改示範案例1中的foo()函數,讓其列印傳入的元素的值,于是設計了下面的代碼:
  • 但是這是錯誤的,編譯不通過
  • 假設val不是指針類型,而代碼中卻有*val的操作,這顯然是錯誤的
template<typename T>
void foo(const T& val)
{
    std::cout << (std::is_pointer<T>::value ? *val : val) << std::endl;
}      
  • 如果想要達到上面的目的,可以借助std::true_type和std::false_type來完成。代碼如下:
#include <type_traits>

//如果是指針,調用這個
template<typename T>
void foo_impl(const T& val,std::true_type)
{
    std::cout << "foo() called for a pointer:" << *val << std::endl;
}
//如果不是指針,調用這個
template<typename T>
void foo_impl(const T& val, std::false_type)
{
    std::cout << "foo() called for a value:" << val << std::endl;
}

template<typename T>
void foo(const T& val)
{
    foo_impl(val, std::is_pointer<T>());
}

int main()
{
    int num = 10;
    int *p = &num;

    foo(num);
    foo(p);
}      
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • 直接調用foo_impl()函數也是可以的。例如:
int main()
{
    int num = 10;
    int *p = &num;

    foo_impl(num, std::is_pointer<int>());
    foo_impl(p, std::is_pointer<int*>());
}      

示範案例3(std::is_integral<>,針對整數類型的彈性重載)

  • 例如現在我們有一批重載函數,一部分是針對于整數類型的,一部分是針對于浮點數類型的。例如:
void foo(short);
void foo(unsigned short);
void foo(int);

void foo(float);
void foo(double);
void foo(long double);      
  • 上面的代碼有很大的缺點:這樣做重複工作很多,代碼比較備援,并且如果加入了新資料類型那麼還需要重新定義新的foo()函數
  • 一種做法是使用trait機制提供的模闆類,例如此處使用std::integral<>模闆。定義的代碼如下:
//針對于整數類型設計的
template<typename T>
void foo_impl(T val, std::true_type);

//針對于浮點數類型設計的
template<typename T>
void foo_impl(T val, std::false_type);

template<typename T>
void foo(T val)
{
    //通過is_integral萃取類型
    //如果T為整數類型,std::is_integral傳回std::true_type;否則傳回std::false_type
    foo_impl(val, std::is_integral<T>());
}      

示範案例4(std::common_type<>,處理通用類型)

  • 假設我們有個函數來比較兩個值的最小值,并将最小值進行傳回,如果T1和T2的資料類型不一緻,那麼傳回值該如何定義哪?
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • 我們可以借助std::common_type<>解決這個問題。例如:
  • 如果傳入的兩個實參都是int,或傳入的都是long,或者傳入的一個是int一個是long,那麼std::common_type<>傳回int,是以下面的min的傳回值為int類型
  • 如果傳入的參數一個是string而另一個是字元串字面常量,那麼下面的min的傳回值為std::string
template<typename T1, typename T2>
typename std::common_type<T1, T2>::type min(const T1& x, const T2& y);      
  • 使用std::common_type<>的前提是,兩個實參它們有共同的資料類型。前提是程式員自己保證的(看下面的實作原理)
  • std::common_type<>的實作原理如下:
  • 其内部使用?:運算符,直接傳回T1的資料類型。是以上面不論min()的T2參數屬于什麼類型,其隻傳回T1所表示的資料類型
  • 其内部使用了一個std::declval<>模闆,特屬于trait的一種,其根據傳入的類型提供一個值,但不去核算它(最終傳回一個該值的rvalue reference)
  • 然後再使用decltype關鍵字導出表達式的類型
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • 通過上面的comm_type<>就可以找出一個共同類型,如果找不到,可以使用common_type<>的重載版本(這正是chrono程式庫的作為,使它得以結合duration;詳情見後面的chrono程式庫介紹)

三、Type Trait的分類

  • Type trait大多數定義于<type_traits>頭檔案中,有些定義在别的頭檔案中(下面會注釋)

①類型判斷式

  • 下圖列出了針對于所有類型都使用的trait:
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • 下圖列出了針對于class類型都使用的trait
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • std::true_type、std::false_type:
  • 上面的類型的傳回值是std::true_type或std::false_type
  • std::true_type和std::false_type都是std::integral_constant的特化,是以它們相應的value成員可以傳回true或false。如下圖所示:
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • 一些注意事項:
  • bool和所有character類型(char、char16_t、char32_t、wchar_t)都屬于整數類型
  • std::nullptr_t為基礎資料類型
  • 上面大部分都是單參數形式的,但并非全部都是
  • 一個“指向const類型”的非常量pointer或reference,其本身并不是一個常量(見下面示範案例)
  • 用以檢驗copy和move語義的那些trait,隻檢驗是否相應的表達式為可能。例如,一個“帶有copy構造函數(接受常量實參)但沒有move構造函數”的類型,仍然是move constructible
  • is_nothrow..type trait特别被用來闡述noexcept異常聲明
  • 下面是is_const<>的示範案例:
std::cout << boolalpha;
std::cout << "is_const<int>::value                 " << is_const<int>::value << std::endl;
std::cout << "is_const<const volatile int>::value  " << is_const<const volatile int>::value << std::endl;
std::cout << "is_const<int* const>::value          " << is_const<int* const>::value << std::endl;
std::cout << "is_const<const int*>::value          " << is_const<const int*>::value << std::endl;
std::cout << "is_const<const int&>::value          " << is_const<const int&>::value << std::endl;
std::cout << "is_const<int[3]>::value              " << is_const<int[3]>::value << std::endl;
std::cout << "is_const<const int[3]>::value        " << is_const<const int[3]>::value << std::endl;
std::cout << "is_const<int[]>::value               " << is_const<int[]>::value << std::endl;
std::cout << "is_const<const int[]>::value         " << is_const<const int[]>::value << std::endl;      
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)

②用以檢驗類型關系的Trait

  • 下圖列出的type trait可以檢查類型之間的關系,包括檢查class type提供了哪一種構造函數和哪一種指派操作等等
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • is_assignable<>的使用注意事項:
  • 注意,基本資料類型(例如int)可以表現出lvalue或是rvalue,是以你不能夠直接指派,例如“42=77”,這是錯誤的。是以is_assignable<>第一個類型如果是一個nonclass類型,永遠會獲得false_type
  • 如果是class類型,以其尋常類型作為第一類型是可以的,因為存在一個有趣的舊規則:你可以調用“類型為class”的rvalue的成員函數。例如:
std::cout << boolalpha;
std::cout << "is_assignable<int,int>::value                 " << is_assignable<int, int>::value << std::endl;
std::cout << "is_assignable<int&,int>::value                " << is_assignable<int&, int>::value << std::endl;
std::cout << "is_assignable<int&&,int>::value               " << is_assignable<int&&, int>::value << std::endl;
std::cout << "is_assignable<long&,int>::value               " << is_assignable<long&, int>::value << std::endl;
std::cout << "is_assignable<int&,void*>::value              " << is_assignable<int&, void*>::value << std::endl;
std::cout << "is_assignable<void*,int>::value               " << is_assignable<void*, int>::value << std::endl;
std::cout << "is_assignable<const char*,std::string>::value " << is_assignable<const char*, std::string>::value << std::endl;
std::cout << "is_assignable<std::string,const char*>::value " << is_assignable<std::string, const char*>::value << std::endl;      
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • 下面是is_constructible<>的示範案例:
std::cout << boolalpha;
std::cout << "is_constructible<int>::value                             " << is_constructible<int>::value << std::endl;
std::cout << "is_constructible<int,int>::value                         " << is_constructible<int, int>::value << std::endl;
std::cout << "is_constructible<long,int>::value                        " << is_constructible<long, int>::value << std::endl;
std::cout << "is_constructible<int,void*>::value                       " << is_constructible<int, void*>::value << std::endl;
std::cout << "is_constructible<void*,int>::value                       " << is_constructible<void*, int>::value << std::endl;
std::cout << "is_constructible<const char*,std::string>::value         " << is_constructible<const char*, std::string>::value << std::endl;
std::cout << "is_constructible<std::string,const char*>::value         " << is_constructible<std::string, const char*>::value << std::endl;
std::cout << "is_constructible<std::string,const char*,int,int>::value " << is_constructible<std::string, const char*, int, int>::value << std::endl;      
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • std::use_allocator<>被定義在<memory>頭檔案中

③類型修飾符

  • 下圖列出的trait允許你改動類型
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • 使用規則:
  • 如果想要為某一類型添加一個屬性,前提是該屬性尚未存在
  • 如果想要為某一類型移除一個屬性,前提是該屬性已經存在
  • 下面是一些示範案例:
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • 類型const int&可被降級或擴充。例如:
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • 一些注意事項:
  • 一個“指向某常量類型”的reference本身并不是常量,是以你不可以移除其常量性
  • add_pointer<>必然包含使用remove_reference<>
  • 然後make_signed<>和make_unsigned<>要求實參若非整數類型就必須是枚舉類型,bool除外,是以如果你傳入reference會導緻不明确行為
  • add_value_reference<>把一個rvalue reference轉換為一個lvalue reference,然而add_rvalue_reference<>并不能把一個lvalue reference轉換為一個rvalue reference(類型保持不變)。是以,必須這麼做才能将一個lvalue轉換為一個rvalue reference:
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)

④其他trait

  • 下圖列出了其餘所有type trait。它們用來查詢特殊屬性、檢查類型關系、或提供更複雜的類型變換
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • decay<>允許你講“以by value傳入”的類型T轉換為其相應類型。以此方式,它轉換array和function類型稱為pointer,把lvalue轉換為rvalue——其中包括移除const和volatile
  • common_type<>為所有被傳入的類型提供一個共同類型(它可以有1個、2個或更多個類型實參)
  • 下面是一些示範案例:
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)

四、Reference Wrapper(外覆器)

  • 聲明于<functional>中的一些類型:
  • std::reference_wrapper<>:可以将傳值調用改為傳reference調用
  • std::ref():可以将類型T隐式轉換為T&
  • std::cref():可以将類型T隐式轉換為const T&
  • 示範案例:
template<typename T>
void foo(T val) { val++; }

int main()
{
    int num1 = 1, num2 = 1;
    
    foo(num1);
    std::cout << "num1:" << num1 << std::endl;

    foo(std::ref(num2)); //改為T&調用
    std::cout << "num2:" << num2 << std::endl;
}      
C++(标準庫):08---Type Trait和Type Utility(type_traits庫)
  • 這些特性被C++标準庫運用于各個地方,例如:
  • make_pair()用此特性,于是能夠建立一個pair<> of references
  • make_tuple()用此特性,于是能夠建立一個tuple<> of references
  • Binder用此特性,于是能夠綁定reference
  • Thread用此特性,于是能夠以by reference形式傳遞實參
  • 注意事項:class reference_wrapper使你得以使用reference作為最進階對象,例如作為array或STL容器的元素類型(示範案例參閱

五、Function Type Wrapper(外覆器)

  • std::function<>:
  • std::function<>聲明于<functional>,提供多态外覆器,可以概括functional pointer記号
  • 這個模闆允許你把可調用對象當做最進階對象
  • std::function<>在另外一篇文章單獨介紹過​
  • 示範案例:
int func(int x, int y)
{
    return x + y;
}

int main()
{
    std::vector<std::function<void(int, int)>> tasks;
    tasks.push_back(func);
    tasks.push_back([](int x, int y) { return x + y; });

    for (std::function<void(int, int)> f : tasks) {
        f(33, 66);
    }
}      
  • 如果使用member function,那麼必須将“調用它們”的那個對象作為參數1進行傳遞。例如:
class C {
public:
    void memfunc(int x, int y) {}
};

int main()
{
    std::function<void(const C&, int, int)> mf;
    mf = &C::memfunc;
    mf(C(), 42, 77);
}      
  • 這個東西的另一個應用:聲明某個函數傳回一個lambda(詳情見《C++标準庫》P31)
  • 注意:執行一個函數調用,卻沒有标的物可調用,将會抛出std::bad_function_call異常。例如:

繼續閱讀