跳至內容

更多 C++ 慣用法/成員檢測器

來自華夏公益教科書,開放的書籍,開放的世界

成員檢測器

[編輯 | 編輯原始碼]

檢測類中是否存在特定成員屬性、函式或型別。

也稱為

[編輯 | 編輯原始碼]

編譯時反射功能是 C++ 模板超程式設計的基石。型別特徵庫,如 Boost.TypeTraits 和 TR1 <type_traits> 標頭,提供了強大的方法來提取有關型別及其關係的資訊。檢測類中是否存在資料成員也是編譯時反射的一個例子。

解決方案和示例程式碼

[編輯 | 編輯原始碼]

成員檢測器慣用法是使用 替換失敗不是錯誤 (SFINAE) 慣用法實現的。以下類模板 DetectX<T> 是一個元函式,它確定型別 T 是否在其內部包含名為 X 的資料或函式成員。請注意,資料成員 X 的型別無關緊要,也不包括成員函式(如果存在)的返回值和引數。

template<typename T>
class DetectX
{
    struct Fallback { int X; }; // add member name "X"
    struct Derived : T, Fallback { };

    template<typename U, U> struct Check;

    typedef char ArrayOfOne[1];  // typedef for an array of size one.
    typedef char ArrayOfTwo[2];  // typedef for an array of size two.

    template<typename U> 
    static ArrayOfOne & func(Check<int Fallback::*, &U::X> *);
    
    template<typename U> 
    static ArrayOfTwo & func(...);

  public:
    typedef DetectX type;
    enum { value = sizeof(func<Derived>(0)) == 2 };
};

這種慣用法透過在編譯期間建立受控歧義並使用 SFINAE 慣用法從這種歧義中恢復來工作。首先,代理類 Fallback 擁有我們想要檢測其存在性的同名數據成員。類 Derived 同時繼承自 TFallback。因此,Derived 類將至少具有一個名為 X 的資料成員。如果 T 也具有 X,則 Derived 類可能具有兩個 X 資料成員。

Check 模板用於建立受控歧義。Check 模板採用兩個引數。第一個是型別引數,第二個是該型別的例項。例如,Check<int, 5> 將是一個有效的例項化。兩個名為 func 的過載函式會建立一個過載集,就像 SFINAE 慣用法中經常做的那樣。第一個 func 函式只有在可以明確獲取資料成員 U::X 的地址時才能例項化。如果 Derived 類中恰好只有一個 X 資料成員,則可以獲取 U::X 的地址;即 T 沒有資料成員 X。如果 T 中有 X,則在沒有進一步區分的情況下無法獲取 U::X 的地址,因此第一個 func 的例項化將失敗,而另一個函式將被選中,所有這些都不會出現錯誤。請注意兩個 func 函式的返回值型別之間的區別。第一個函式返回對大小為一的陣列的引用,而第二個函式返回對大小為二的陣列的引用。這種大小上的差異使我們能夠識別哪個函式被例項化。

最後,公開了一個布林值 value,它僅在函式返回值型別的 sizeof 為二時才為真。也就是說,當第二個 func 僅因為 T 具有 X 資料成員而被例項化時。

對於要檢測的每個不同的成員,上述類模板都需要更改。在這種情況下,宏會更可取。以下示例程式碼演示了宏的使用。

#define CREATE_MEMBER_DETECTOR(X)                                                   \
template<typename T> class Detect_##X {                                             \
    struct Fallback { int X; };                                                     \
    struct Derived : T, Fallback { };                                               \
                                                                                    \
    template<typename U, U> struct Check;                                           \
                                                                                    \
    typedef char ArrayOfOne[1];                                                     \
    typedef char ArrayOfTwo[2];                                                     \
                                                                                    \
    template<typename U> static ArrayOfOne & func(Check<int Fallback::*, &U::X> *); \
    template<typename U> static ArrayOfTwo & func(...);                             \
  public:                                                                           \
    typedef Detect_##X type;                                                        \
    enum { value = sizeof(func<Derived>(0)) == 2 };                                 \
};

CREATE_MEMBER_DETECTOR(first);
CREATE_MEMBER_DETECTOR(second);

int main(void)
{
  typedef std::pair<int, double> Pair;
  std::cout << ((Detect_first<Pair>::value && Detect_second<Pair>::value)? "Pair" : "Not Pair");
}

使用 C++11 功能,可以重寫此示例,以便使用 decltype 說明符而不是 Check 模板和指向成員的指標來建立受控歧義。然後,可以將結果包裝在從 integral_constant 繼承的類中,以提供與標準標頭 <type_traits> 中存在的型別謂詞相同的介面。

#include <type_traits> // To use 'std::integral_constant'.
#include <iostream>    // To use 'std::cout'.
#include <iomanip>     // To use 'std::boolalpha'.


#define GENERATE_HAS_MEMBER(member)                                               \
                                                                                  \
template < class T >                                                              \
class HasMember_##member                                                          \
{                                                                                 \
private:                                                                          \
    using Yes = char[2];                                                          \
    using  No = char[1];                                                          \
                                                                                  \
    struct Fallback { int member; };                                              \
    struct Derived : T, Fallback { };                                             \
                                                                                  \
    template < class U >                                                          \
    static No& test ( decltype(U::member)* );                                     \
    template < typename U >                                                       \
    static Yes& test ( U* );                                                      \
                                                                                  \
public:                                                                           \
    static constexpr bool RESULT = sizeof(test<Derived>(nullptr)) == sizeof(Yes); \
};                                                                                \
                                                                                  \
template < class T >                                                              \
struct has_member_##member                                                        \
: public std::integral_constant<bool, HasMember_##member<T>::RESULT>              \
{                                                                                 \
};


GENERATE_HAS_MEMBER(att)  // Creates 'has_member_att'.
GENERATE_HAS_MEMBER(func) // Creates 'has_member_func'.


struct A
{
    int att;
    void func ( double );
};

struct B
{
    char att[3];
    double func ( const char* );
};

struct C : A, B { }; // It will also work with ambiguous members.


int main ( )
{
    std::cout << std::boolalpha
              << "\n" "'att' in 'C' : "
              << has_member_att<C>::value // <type_traits>-like interface.
              << "\n" "'func' in 'C' : "
              << has_member_func<C>() // Implicitly convertible to 'bool'.
              << "\n";
}


檢測成員型別

上述示例可以適應檢測成員型別的存在,無論是巢狀類還是 typedef,即使它是未完成的。這一次,歧義將透過向 Fallback 新增一個與宏引數同名的巢狀型別來建立。

#include <type_traits> // To use 'std::integral_constant'.
#include <iostream>    // To use 'std::cout'.
#include <iomanip>     // To use 'std::boolalpha'.


#define GENERATE_HAS_MEMBER_TYPE(Type)                                            \
                                                                                  \
template < class T >                                                              \
class HasMemberType_##Type                                                        \
{                                                                                 \
private:                                                                          \
    using Yes = char[2];                                                          \
    using  No = char[1];                                                          \
                                                                                  \
    struct Fallback { struct Type { }; };                                         \
    struct Derived : T, Fallback { };                                             \
                                                                                  \
    template < class U >                                                          \
    static No& test ( typename U::Type* );                                        \
    template < typename U >                                                       \
    static Yes& test ( U* );                                                      \
                                                                                  \
public:                                                                           \
    static constexpr bool RESULT = sizeof(test<Derived>(nullptr)) == sizeof(Yes); \
};                                                                                \
                                                                                  \
template < class T >                                                              \
struct has_member_type_##Type                                                     \
: public std::integral_constant<bool, HasMemberType_##Type<T>::RESULT>            \
{ };                                                                              \


GENERATE_HAS_MEMBER_TYPE(Foo) // Creates 'has_member_type_Foo'.


struct A
{
    struct Foo;
};

struct B
{
    using Foo = int;
};

struct C : A, B { }; // Will also work on incomplete or ambiguous types.


int main ( )
{
    std::cout << std::boolalpha
              << "'Foo' in 'C' : "
              << has_member_type_Foo<C>::value
              << "\n";
}

這很像宣告與宏引數同名的成員資料的成員檢測器宏。然後建立一個由兩個 test 函式組成的過載集,就像其他示例一樣。第一個版本只有在可以明確使用 U::Type 的型別時才能例項化。此型別只有在 Derived 中恰好有一個 Type 例項時才能使用,即 T 中沒有 Type。如果 T 具有成員型別 Type,它保證與 Fallback::Type 不同,因為後者是一個唯一的型別,因此會造成歧義。如果替換失敗,這會導致 test 的第二個版本被例項化,這意味著 T 確實具有成員型別 Type。由於從未建立過任何物件(這完全由編譯時型別檢查解決),因此 Type 可以是一個不完整的型別,或者在 T 內部是模稜兩可的;只有名稱很重要。然後我們將結果包裝在從 integral_constant 繼承的類中,就像之前一樣,以提供與標準庫相同的介面。


檢測過載成員函式

成員檢測器慣用法的變體可以用於檢測類中特定成員函式的存在,即使它被過載了。

template<typename T, typename RESULT, typename ARG1, typename ARG2>
class HasPolicy
{
    template <typename U, RESULT (U::*)(ARG1, ARG2)> struct Check;
    template <typename U> static char func(Check<U, &U::policy> *);
    template <typename U> static int func(...);
  public:
    typedef HasPolicy type;
    enum { value = sizeof(func<T>(0)) == sizeof(char) };
};

上面的 HasPolicy 模板檢查 T 是否具有名為 policy 的成員函式,該函式接受兩個引數 ARG1ARG2 並返回 RESULT。僅當 U 具有接受兩個引數並返回 RESULTU::policy 成員函式時,Check 模板的例項化才會成功。請注意,Check 模板的第一個型別引數是型別,而第二個引數是同一型別中成員函式的指標。如果 Check 模板無法例項化,則僅會例項化返回 int 的剩餘 funcfunc 的返回值的大小最終確定型別特徵的答案:true 或 false。

已知問題

[編輯 | 編輯原始碼]

如果檢查成員的類被宣告為 final(C++11 關鍵字),則不起作用。

無法用於檢查聯合的成員(聯合不能作為基類)。

C++17 添加了 void_t 以便於執行超程式設計任務。void_t<Ts...> 只是擴充套件到 void,但由於 SFINAE,它仍然很有用。使用這種方法的好處是它解決了不支援 final 類和聯合的已知問題。

namespace detail
{
    template <class T, class = void>
    constexpr bool has_X = false;
    template <class T>
    // `void_t<Ts...>` expands to `void` if all `Ts...` are valid types, but if
    // any of them are invalid, substitution fails, which here triggers SFINAE
    constexpr bool has_X<T, std::void_t<decltype(&T::X)>> = true;
}

template <class T>
struct DetectX: public std::bool_constant<detail::has_X<T>>
{
};

C++20 添加了 requires 語句,它比上面簡單的成員檢查器功能強大得多,因為它可以檢查更復雜表示式的有效性,但它也可以用作成員檢查器。由於其簡單性,它還使程式碼更簡單、更易讀。

template <class T>
struct DetectX: public std::bool_constant<requires {&T::X;}>
{
};

template <class T>
// Derives from `std::false_type` if `T::X` is a method, unlike the above class 
// which does not require `T::X` to be a data member, but allows methods also.
struct DetectXOnlyIfMember: public std::bool_constant<requires (T t){t.X;}>
{
};
[編輯 | 編輯原始碼]

參考文獻

[編輯 | 編輯原始碼]
華夏公益教科書