跳至内容

更多 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;}>
{
};
[编辑 | 编辑源代码]

参考文献

[编辑 | 编辑源代码]
华夏公益教科书