使用可变参数测试成员函数是否存在

14

所以我非常熟悉测试成员函数是否存在的范式。目前这段代码是有效的:

#include <iostream>
#include <type_traits>

struct has_mem_func_foo_impl {
    template <typename U, U>
    struct chk { };

    template <typename Class, typename Arg>
    static std::true_type has_foo(chk<void(Class::*)(Arg), &Class::foo>*);

    template <typename, typename>
    static std::false_type has_foo(...);
};

template <typename Class, typename Arg>
struct has_mem_func_foo : decltype(has_mem_func_foo_impl::template has_foo<Class,Arg>(nullptr)) { };


struct bar {
    void foo(int) { }
};

int main() {
    static_assert( has_mem_func_foo<bar, int>::value, "bar has foo(int)" );
}

不幸的是,如果我进行轻微的调整:

#include <iostream>
#include <type_traits>

struct has_mem_func_foo_impl {
    template <typename U, U>
    struct chk { };

    template <typename Class, typename... Arg>
    static std::true_type has_foo(chk<void(Class::*)(Arg...), &Class::foo>*);

    template <typename, typename...>
    static std::false_type has_foo(...);
};

template <typename Class, typename... Arg>
struct has_mem_func_foo : decltype(has_mem_func_foo_impl::template has_foo<Class,Arg...>(nullptr)) { };


struct bar {
    void foo(int) { }
};

int main() {
    static_assert( has_mem_func_foo<bar, int>::value, "bar has foo(int)" );
}

我的静态断言失败了。我曾经认为可变模板参数包在展开到它们的位置时会被同等对待。无论是gcc还是clang都会产生失败的静态断言。

因此,我的问题的真正根源是,这是标准行为吗?当测试可变模板成员函数是否存在时也会失败。


所以我看到的问题是编译器无法保证不会推导出更多 ...Arg。您传递了第一个 Arg,但它还需要针对每个其他的 Arg 进行检查。嗯。如果这是正确的,我想我有一个主意。 - Yakk - Adam Nevraumont
解决方案是将 typename... Arg 移动到 has_mem_func_foo_impl 模板参数中,像这样 - Piotr Skotnicki
我确实可以看到会发生这种情况,但在这种情况下我们显式传递了参数包int - cdacamara
@cdacamara 不,你需要传递参数包的第一个元素:你不能告诉它参数包已经结束。因此它不能假设参数包已经结束。编译器可以自由推断更多内容,所以它尝试进行推断,但无法从 nullptr_t 推断出来,于是放弃了该重载。 - Yakk - Adam Nevraumont
1个回答

8
我看到的问题是传递给 Arg...int 不足够。编译器可以向其添加新的参数,这是有效的。从 nullptr_t 推断添加到其末尾的内容是不可能的,因此编译器会说“我放弃了,不处理这种情况”。但即使我们无法在可推断的上下文中使用 Arg...,你的技巧仍然有效。请见这里
#include <iostream>
#include <type_traits>

template<class Sig>
struct has_mem_func_foo_impl;

template<class R, class...Args>
struct has_mem_func_foo_impl<R(Args...)> {
  template <typename U, U>
  struct chk { };

  template <typename Class>
  static constexpr std::true_type has_foo(chk<R(Class::*)(Args...), &Class::foo>*) { return {}; }

  template <typename>
  static constexpr std::false_type has_foo(...) { return {}; }
};

template <typename Class, typename Sig>
struct has_mem_func_foo :
  decltype(has_mem_func_foo_impl<Sig>::template has_foo<Class>(nullptr))
{};

struct bar {
  void foo(int) { }
};


int main() {
  static_assert( has_mem_func_foo<bar, void(int)>::value, "bar has foo(int)" );
}

我们将Args...移动到类本身,然后只将类型传递给函数。这会阻止推导,使nullptr转换为成员函数指针变得可行,并且可以正常工作。
我还包括了一些改进的基于签名的语法,这也意味着它支持返回类型匹配。
请注意,您可能在问错误的问题。您正在询问是否存在具有特定签名的成员函数:通常您想知道是否存在可使用一组特定参数调用并且返回类型与您的返回值兼容的成员函数。
namespace details {
  template<class T, class Sig, class=void>
  struct has_foo:std::false_type{};

  template<class T, class R, class... Args>
  struct has_foo<T, R(Args...),
    typename std::enable_if<
      std::is_convertible<
        decltype(std::declval<T>().foo(std::declval<Args>()...)),
        R
      >::value
      || std::is_same<R, void>::value // all return types are compatible with void
      // and, due to SFINAE, we can invoke T.foo(Args...) (otherwise previous clause fails)
    >::type
  >:std::true_type{};
}
template<class T, class Sig>
using has_foo = std::integral_constant<bool, details::has_foo<T, Sig>::value>;

这段代码试图调用T.foo(int),并检查返回值是否兼容。

为了好玩,我将has_foo的类型实际上设置为true_typefalse_type,而不是从中继承。我可以直接使用:

template<class T, class Sig>
using has_foo = details::has_foo<T, Sig>;

如果我不需要那个额外的功能。


1
我不确定我理解了,如果您不介意解释一下...为什么这里甚至需要猜测任何东西?Args...在这里显然是int,对吧?这个可以工作,我认为编译器可用的信息量完全相同。 - jrok
就像@Yakk所解释的那样,我只向一个推导函数上下文传递了参数包的第一个元素,这就像是部分特化,编译器无法保证在这种情况下没有更多要推导的内容,或者后续的特化(与对象模板进行比较)。 - cdacamara
@jrok 不确定。也许默认参数不能以相同的方式引起推断?当我删除了false_type重载时,从错误消息中找出了困扰编译器的原因。 - Yakk - Adam Nevraumont
@Yakk 我想我现在已经理解了。而且类型推断在默认参数中根本不起作用,所以我猜编译器只是采取了它得到的东西,因为 Args... 不能再扩展了? +1 现在,如果您还能提供管理此情况的标准引用,那就更好了... :) - jrok
@Yakk,我认为你的新解决方案唯一的问题是没有办法检测成员函数是否带有const限定符。例如,如果我只想要void foo() const而不是void foo(),你无法区分这两者。 - cdacamara
1
请将 const Foo 作为类型传递给 @cdacamara。请注意,默认情况下它是右值,如果需要左值重载检测,请加上 & - Yakk - Adam Nevraumont

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接