匹配继承的成员函数的类型

Match type of inherited member functions

本文关键字:函数 类型 成员 继承      更新时间:2023-10-16

我有以下代码片段,无法编译:

#include <iostream>
struct A {
    void foo() {}
};
struct B : public A {
    using A::foo;
};
template<typename U, U> struct helper{};
int main() {
    helper<void (A::*)(), &A::foo> compiles;
    helper<void (B::*)(), &B::foo> does_not_compile;
    return 0;
}

由于&B::foo解析为&A::foo,因此无法编译,因此无法匹配提议的类型void (B::*)()。由于这是我用来检查非常特定接口的SFINAE模板的一部分(我强制使用特定的参数类型和输出类型),因此我希望它独立于继承而工作,同时保持检查可读。

我尝试的包括:

  • 强制转换参数的第二部分:

    helper<void (B::*)(), (void (B::*)())&B::foo> does_not_compile;

    不幸的是,这没有帮助,因为第二部分现在不被识别为常量表达式,并且失败。

  • 我已经尝试将引用分配给一个变量,以便检查。

    constexpr void (B::* p)() = &B::foo; helper<void (B::* const)(), p> half_compiles;

    这个代码被clang 3.4接受了,但是g++ 4.8.1拒绝了,我不知道谁是对的。

任何想法?

编辑:由于许多评论要求更具体的问题版本,我将在这里写下来:

我正在寻找的是一种显式检查类是否遵守特定接口的方法。此检查将用于验证模板化函数中的输入参数,以便它们尊重这些函数所需的契约,以便在类和函数不兼容的情况下提前停止编译(即类型特征类型的检查)。

因此,我需要能够验证我请求的每个成员函数的返回类型、参数类型和编号、constness等。最初的问题是我用来验证匹配的更大模板的检查部分。

下面给出了在https://ideone.com/mxIVw3上发布的问题的工作解决方案-参见另一个实例。

这个问题在某种意义上是c++中推导继承方法的父类的后续问题。在我的回答中,我定义了一个类型trait member_class,它从给定的指向成员函数类型的指针中提取一个类。下面我们将使用更多的特征来分析并合成这样的类型。

首先member_type提取签名,例如void (C::*)()给出void():

template <typename M> struct member_type_t { };
template <typename M> using  member_type = typename member_type_t <M>::type;
template <typename T, typename C>
struct member_type_t <T C::*> { using type = T;};

然后,member_class提取类,例如void (C::*)()给出C:

template<typename>
struct member_class_t;
template<typename M>
using member_class = typename member_class_t <M>::type;
template<typename R, typename C, typename... A>
struct member_class_t <R(C::*)(A...)> { using type = C; };
template<typename R, typename C, typename... A>
struct member_class_t <R(C::*)(A...) const> { using type = C const; };
// ...other qualifier specializations

最后,member_ptr合成一个指向成员函数类型的指针,给出一个类和一个签名,例如C + void()给出void (C::*)():

template <typename C, typename S>
struct member_ptr_t;
template <typename C, typename S>
using member_ptr = typename member_ptr_t <C, S>::type;
template <typename C, typename R, typename ...A>
struct member_ptr_t <C, R(A...)> { using type = R (C::*)(A...); };
template <typename C, typename R, typename ...A>
struct member_ptr_t <C const, R(A...)> { using type = R (C::*)(A...) const; };
// ...other qualifier specializations

前两个特征需要更多的专门化,以使不同的限定符更通用,例如const/volatile或ref-qualifiers。有12种组合(或13种包括数据成员);完整的实现在这里。

其思想是,任何限定符都由member_class从指向成员函数类型的指针转移到类本身。然后member_ptr将限定符从类传回指针类型。当限定符在类类型上时,可以自由地操作标准特征,例如添加或删除const,左值/右值引用等。 现在,这是你的is_foo测试:
template <typename T>
struct is_foo {
private:
    template<
        typename Z,
        typename M = decltype(&Z::foo),
        typename C = typename std::decay<member_class<M>>::type,
        typename S = member_type<M>
    >
    using pattern = member_ptr<C const, void()>;
    template<typename U, U> struct helper{};
    template <typename Z> static auto test(Z z) -> decltype(
        helper<pattern<Z>, &Z::foo>(),
        // All other requirements follow..
        std::true_type()
    );
    template <typename> static auto test(...) -> std::false_type;
public:
    enum { value = std::is_same<decltype(test<T>(std::declval<T>())),std::true_type>::value };
};

给定类型Z,别名模板pattern通过decltype(&Z::foo)获取成员指针的正确类型M,提取其decay的类C和签名S,合成一个新的类C const和签名void()的指针指向成员函数类型,即void (C::*)() const。这正是您所需要的:它与原始硬编码模式相同,将类型Z替换为正确的类C(可能是基类),如decltype所示。

图形:

M = void (Z::*)() const  ->   Z         +   void()
                         ->   Z const   +   void()
                         ->   void (Z::*)() const   ==   M
                         ->   SUCCESS
M = int (Z::*)() const&  ->   Z const&   +   int()
                         ->   Z const    +   void()
                         ->   void (Z::*)() const   !=   M
                         ->   FAILURE

事实上,这里不需要签名S,所以member_type也不需要。但是我在这个过程中使用了它,所以我把它包括在这里是为了完整。它可能在更一般的情况下有用。

当然,所有这些对于多重重载不起作用,因为decltype在这种情况下不起作用。

如果您只是想检查给定类型T上的接口是否存在,那么还有更好的方法。下面是一个例子:

template<typename T>
struct has_foo
{
    template<typename U>
    constexpr static auto sfinae(U *obj) -> decltype(obj->foo(), bool()) { return true; }
    constexpr static auto sfinae(...) -> bool { return false; }
    constexpr static bool value = sfinae(static_cast<T*>(0));
};

测试代码:

struct A {
    void foo() {}
};
struct B : public A {
    using A::foo;
};
struct C{};
int main() 
{
    std::cout << has_foo<A>::value << std::endl;
    std::cout << has_foo<B>::value << std::endl;
    std::cout << has_foo<C>::value << std::endl;
    std::cout << has_foo<int>::value << std::endl;
    return 0;
}
输出(演示):

1
1
0
0

希望对你有帮助。

下面是一个通过测试的简单类(并且不需要大量的专门化:))。当foo过载时,它也可以工作。您希望检查的签名也可以是模板参数(这是一件好事,对吗?)

#include <type_traits>
template <typename T>
struct is_foo {
    template<typename U>
    static auto check(int) ->
    decltype( static_cast< void (U::*)() const >(&U::foo), std::true_type() );
    //                     ^^^^^^^^^^^^^^^^^^^
    //                     the desired signature goes here
    template<typename>
    static std::false_type check(...);
    static constexpr bool value = decltype(check<T>(0))::value;
};

编辑:

我们有两个check的重载。两者都可以接受整数字量作为参数,因为第二个方法在参数列表中有一个省略号,当两个重载都可行时,它永远不会是重载解析的最佳可行方法( ellipsis -conversion-sequence比任何其他转换序列都差)。这使得我们稍后可以明确地初始化trait类的value成员。

只有当第一个过载从过载集中被丢弃时才选择第二个过载。当模板参数替换失败且不是错误(SFINAE)时,会发生这种情况。

这是decltype中逗号操作符左侧的时髦表达式,使其发生。当

  1. 子表达式&U::foo格式错误,这可能发生在

    • U不是类类型,或者
    • U::foo不可访问,或者
    • 没有U::foo
  2. 结果成员指针不能是static_cast到目标类型

注意,当U::foo本身是不明确的时候,查找&U::foo不会失败。这在c++标准13.4 (重载函数的地址,[over.over])中列出的特定上下文中是有保证的。一个这样的上下文是显式类型转换(在本例中为static_cast)。

表达式还利用了T B::*可转换为T D::*的事实,其中D是从B派生的类(但不是相反)。这样就不需要像iavr的答案那样推断类类型了。

然后用true_typefalse_type中的value初始化value成员
但是,这个解决方案有一个潜在的问题。考虑:
struct X {
    void foo() const;
};
struct Y : X {
    int foo();   // hides X::foo
};

现在is_foo<Y>::value将给出false,因为foo的名称查找将在遇到Y::foo时停止。如果这不是您想要的行为,请考虑将您希望在其中执行查找的类作为is_foo的模板参数传递,并使用它来代替&U::foo

希望有帮助。

我建议使用decltype来通用地确定成员函数指针的类型:

helper<decltype(&A::foo), &A::foo> compiles;
helper<decltype(&B::foo), &B::foo> also_compiles;

这似乎违反了DRY,但从根本上说,重复名称并不比单独指定类型更糟糕。