【问题标题】:Find out whether a C++ object is callable查明 C++ 对象是否可调用
【发布时间】:2013-03-01 21:31:32
【问题描述】:

是否可以编写一个类型特征,比如is_callable<T>,它告诉对象是否定义了operator()? 如果事先知道调用运算符的参数很容易,但不是在一般情况下。 当且仅当至少定义了一个重载调用运算符时,我希望特征返回 true。

This question 是相关的并且有一个很好的答案,但它不适用于所有类型(仅适用于 int-convertible 类型)。此外,std::is_function 有效,但仅适用于正确的 C++ 函数,而不适用于仿函数。我正在寻找更通用的解决方案。

【问题讨论】:

  • This 可能是相关的
  • 您有可能的参数类型列表吗?如果是这样,那绝对有可能。不过,不太确定泛型重载。
  • 为什么需要这个?我的意思是,如果您不知道任何参数类型,为什么要知道某些东西是否可以调用?如果您不了解它们,就无法处理诸如重载运算符之类的事情。
  • @mfontanini:基本上是为了区分“立即”值和“惰性”值,例如回调、函子、lambda 表达式...
  • @einpoklum:这是关于operator()(...) 而不是operator()(),所以是的,任何变体。这就是我在问题第二句话中的意思。

标签: c++ metaprogramming traits typetraits


【解决方案1】:

C++17 带来了std::is_invocable 和朋友们。

This answer 还给出了如何用 C++14 模拟它的解决方案。

【讨论】:

    【解决方案2】:

    这是一种使用 C++11 的可能解决方案,该解决方案无需知道函子的调用运算符的签名即可工作,但前提是函子的重载不超过一个 operator ()

    #include <type_traits>
    
    template<typename T, typename = void>
    struct is_callable : std::is_function<T> { };
    
    template<typename T>
    struct is_callable<T, typename std::enable_if<
        std::is_same<decltype(void(&T::operator())), void>::value
        >::type> : std::true_type { };
    

    这就是你将如何使用它:

    struct C
    {
        void operator () () { }
    };
    
    struct NC { };
    
    struct D
    {
        void operator () () { }
        void operator () (int) { }
    };
    
    int main()
    {
        static_assert(is_callable<C>::value, "Error");
        static_assert(is_callable<void()>::value, "Error");
    
        auto l = [] () { };
        static_assert(is_callable<decltype(l)>::value, "Error");
    
        // Fires! (no operator())
        static_assert(is_callable<NC>::value, "Error");
    
        // Fires! (several overloads of operator ())
        static_assert(is_callable<D>::value, "Error");
    }
    

    【讨论】:

    • 嗯...重载运算符的限制确实很限制,但是您的解决方案非常紧凑。
    • 你在这里介绍了函数指针吗?在我看来不是。引用类型也不包括在内。
    【解决方案3】:

    这是查找 T 是否可调用的巧妙而简短的技巧。它与 Walter E. Brown 在 CPPCON'14 在关于现代模板元编程的演讲中最初提出的思路一致。

    template <class... >
    using void_t = void;
    
    template <class T>
    using has_opr_t = decltype(&T::operator());
    
    template <class T, class = void>
    struct is_callable : std::false_type { };
    
    template <class T>
    struct is_callable<T, void_t<has_opr_t<typename std::decay<T>::type>>> : std::true_type { };
    

    【讨论】:

    • 我相信如果operator() 被重载或模板化,这将会中断。
    【解决方案4】:

    我认为这个特性可以满足您的需求。它检测带有任何类型签名的operator(),即使它已超载并且是否已模板化:

    template<typename T>
    struct is_callable {
    private:
        typedef char(&yes)[1];
        typedef char(&no)[2];
    
        struct Fallback { void operator()(); };
        struct Derived : T, Fallback { };
    
        template<typename U, U> struct Check;
    
        template<typename>
        static yes test(...);
    
        template<typename C>
        static no test(Check<void (Fallback::*)(), &C::operator()>*);
    
    public:
        static const bool value = sizeof(test<Derived>(0)) == sizeof(yes);
    };
    

    原理基于Member Detector idiom。实际上,如果您将其传递给非类类型,它将无法编译,但这应该不难修复,为了简洁起见,我只是将其省略了。您还可以将其扩展为对函数报告 true。

    当然,它不会给你任何关于operator() 签名的信息,但我相信这不是你想要的,对吧?

    编辑Klaim

    它很简单,可以使用非类类型(返回 false)。如果将上面的类重命名为is_callable_impl,可以这样写,例如:

    template<typename T>
    struct is_callable
        : std::conditional<
            std::is_class<T>::value,
            is_callable_impl<T>,
            std::false_type
        >::type
    { };
    

    【讨论】:

    • 很好,但如果类型不可继承,则不会编译,例如 int。我不确定如何修改它以使其在这种情况下工作(我猜应该返回 false)。
    • 请将您的解决方案提交到 boost 类型特征库
    • @batbrat 首先,template&lt;typename U, U&gt; struct Check; 声明了一个模板,该模板采用 type该类型的一个实例Check&lt;Fallback, &amp;C::operator()&gt; 无效,因为指向成员的指针显然不是 Fallback 类型。
    • @batbrat 那么,当T 没有成员operator() 时会发生什么?编译器,在检查 test 的第二次重载时
    • @batbrat ...是可行的,在FallbackDerived 基类中看到一个Derived::operator(),检查它是否可以转换为Fallback::operator()(它是)并愉快地选择重载。所以sizeof(test&lt;Derived&gt;(0)) == sizeof(yes) 的结果为真。
    【解决方案5】:

    这是另一个实现。

    它使用std::is_function模板来实现免费功能。

    对于类,它使用类似于Member Detector Idiom 的东西。如果T 有一个呼叫运算符,则callable_2 将包含多个operator()。这将导致第一个can_call 函数由于decltype(&amp;callable_2&lt;T&gt;::operator()) 中的歧义失败而被丢弃(通过SFINAE),第二个can_call 函数将返回true。如果T 没有调用运算符,则将使用第一个can_call 函数(由于重载解析规则)。

    namespace impl
    {
    struct callable_1 { void operator()(); };
    template<typename T> struct callable_2 : T, callable_1 { };
    
    template<typename T>
    static constexpr bool can_call(decltype(&callable_2<T>::operator())*) noexcept { return false; }
    
    template<typename>
    static constexpr bool can_call(...) noexcept { return true; }
    
    template<bool is_class, typename T>
    struct is_callable : public std::is_function<T> { };
    
    template<typename T> struct is_callable<false, T*> : public is_callable<false, T> { };
    template<typename T> struct is_callable<false, T* const> : public is_callable<false, T> { };
    template<typename T> struct is_callable<false, T* volatile> : public is_callable<false, T> { };
    template<typename T> struct is_callable<false, T* const volatile> : public is_callable<false, T> { };
    
    template<typename T>
    struct is_callable<true, T> : public std::integral_constant<bool, can_call<T>(0)> { };
    }
    
    template<typename T>
    using is_callable = impl::is_callable<std::is_class<std::remove_reference_t<T>>::value,
                                        std::remove_reference_t<T>>;
    

    【讨论】:

      【解决方案6】:

      当然,已经有其他几个答案,它们很有用,但似乎没有一个能涵盖 AFAICT 的所有用例。借用这些答案和this possible implementation of std::is_function,我创建了一个版本,涵盖了我能想到的所有可能的用例。它有点冗长,但功能非常完整 (Demo)。

      template<typename T, typename U = void>
      struct is_callable
      {
          static bool const constexpr value = std::conditional_t<
              std::is_class<std::remove_reference_t<T>>::value,
              is_callable<std::remove_reference_t<T>, int>, std::false_type>::value;
      };
      
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args...), U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(*)(Args...), U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(&)(Args...), U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args......), U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(*)(Args......), U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(&)(Args......), U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args...)const, U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args...)volatile, U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args...)const volatile, U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args......)const, U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args......)volatile, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args......)const volatile, U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args...)&, U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args...)const&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args...)volatile&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args...)const volatile&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args......)&, U> : std::true_type {};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args......)const&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args......)volatile&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args......)const volatile&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args...)&&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args...)const&&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args...)volatile&&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args...)const volatile&&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args......)&&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args......)const&&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args......)volatile&&, U> : std::true_type{};
      template<typename T, typename U, typename ...Args>
      struct is_callable<T(Args......)const volatile&&, U> : std::true_type{};
      
      template<typename T>
      struct is_callable<T, int>
      {
      private:
          using YesType = char(&)[1];
          using NoType = char(&)[2];
      
          struct Fallback { void operator()(); };
      
          struct Derived : T, Fallback {};
      
          template<typename U, U>
          struct Check;
      
          template<typename>
          static YesType Test(...);
      
          template<typename C>
          static NoType Test(Check<void (Fallback::*)(), &C::operator()>*);
      
      public:
          static bool const constexpr value = sizeof(Test<Derived>(0)) == sizeof(YesType);
      };
      

      这适用于非类类型(当然返回 false)、函数类型()、函数指针类型、函数引用类型、函子类类型、绑定表达式、lambda 类型等。即使类构造函数是私有的和/或非默认的,即使 operator() 被重载,也能正常工作。这会为成员函数指针设计返回 false,因为它们不可调用,但您可以使用 bind 来创建可调用表达式。

      【讨论】:

      • 注意:Visual Studio 2015 似乎被“可变参数函数”重载(“Args ......”)窒息,我承认我不完全确定这意味着什么,或者为什么它有这种语法。
      • 其实我做了一些进一步的测试,我想我明白了。如果定义了诸如template&lt;typename ...Args&gt; void bar(Args &amp;&amp;...args, ...); 之类的函数(尽管为什么要将可变参数函数模板与不安全的可变参数... 混合在一起,我无法理解!),那么is_callable&lt;decltype(bar&lt;&gt;)&gt; 将引用Args...... 重载。因为Args 参数包可能为空,所以您不能键入Args..., ... 作为参数列表,如果它不为空,那么它将自动为您插入必要的逗号(显然,在 Visual Studio 2015 中除外)。
      【解决方案7】:

      这里的答案很有帮助,但我来到这里想要一些东西,它也可以发现某些东西是否是可调用的,无论它碰巧是一个对象还是一个经典函数。 jrok's answer 这方面的问题,唉,没用,因为 std::conditional 实际上评估了两个手臂的类型!

      所以,这里有一个解决方案:

      // Note that std::is_function says that pointers to functions
      // and references to functions aren't functions, so we'll make our 
      // own is_function_t that pulls off any pointer/reference first.
      
      template<typename T>
      using remove_ref_t = typename std::remove_reference<T>::type;
      
      template<typename T>
      using remove_refptr_t = typename std::remove_pointer<remove_ref_t<T>>::type;
      
      template<typename T>
      using is_function_t = typename std::is_function<remove_refptr_t<T>>::type;
      
      // We can't use std::conditional because it (apparently) must determine
      // the types of both arms of the condition, so we do it directly.
      
      // Non-objects are callable only if they are functions.
      
      template<bool isObject, typename T>
      struct is_callable_impl : public is_function_t<T> {};
      
      // Objects are callable if they have an operator().  We use a method check
      // to find out.
      
      template<typename T>
      struct is_callable_impl<true, T> {
      private:
          struct Fallback { void operator()(); };
          struct Derived : T, Fallback { };
      
          template<typename U, U> struct Check;
      
          template<typename>
          static std::true_type test(...);
      
          template<typename C>
          static std::false_type test(Check<void (Fallback::*)(), &C::operator()>*);
      
      public:
          typedef decltype(test<Derived>(nullptr)) type;
      };
      
      
      // Now we have our final version of is_callable_t.  Again, we have to take
      // care with references because std::is_class says "No" if we give it a
      // reference to a class.
      
      template<typename T>
      using is_callable_t = 
          typename is_callable_impl<std::is_class<remove_ref_t<T>>::value,
                                    remove_ref_t<T> >::type;
      

      但最后,对于我的应用程序,我真的很想知道你是否可以说 f()(即不带参数调用它),所以我选择了更简单的方法。

      template <typename T>
      constexpr bool noarg_callable_impl(
          typename std::enable_if<bool(sizeof((std::declval<T>()(),0)))>::type*)
      {
          return true;
      }
      
      template<typename T>
      constexpr bool noarg_callable_impl(...)
      {
          return false;
      }
      
      template<typename T>
      constexpr bool is_noarg_callable()
      {
          return noarg_callable_impl<T>(nullptr);
      }
      

      事实上,我走得更远。我知道该函数应该返回一个int,所以我不仅检查我是否可以调用它,还检查了返回类型,方法是将enable_if 更改为:

          typename std::enable_if<std::is_convertible<decltype(std::declval<T>()()),
                                                      int>::value>::type*)
      

      希望这对某人有所帮助!

      【讨论】:

        【解决方案8】:

        注意:这些假设默认构造函数对您检查的类型有效。不确定如何解决这个问题。

        如果可以使用 0 个参数调用,则以下内容似乎有效。 is_function 的实现中是否有一些东西可能有助于将其扩展到 1 个或多个参数可调用对象?:

        template <typename T>
        struct is_callable {
            // Types "yes" and "no" are guaranteed to have different sizes,
            // specifically sizeof(yes) == 1 and sizeof(no) == 2.
            typedef char yes[1];
            typedef char no[2];
        
            template <typename C>
            static yes& test(decltype(C()())*);
        
            template <typename>
            static no& test(...);
        
            // If the "sizeof" the result of calling test<T>(0) would be equal to the     sizeof(yes),
            // the first overload worked and T has a nested type named foobar.
            static const bool value = sizeof(test<T>(0)) == sizeof(yes);
        };   
        

        如果您知道参数的类型(即使它是模板参数),以下内容适用于 1 个参数,我想可以很容易地从那里扩展:

        template <typename T, typename T2>
        struct is_callable_1 {
            // Types "yes" and "no" are guaranteed to have different sizes,
            // specifically sizeof(yes) == 1 and sizeof(no) == 2.
            typedef char yes[1];
            typedef char no[2];
        
            template <typename C>
            static yes& test(decltype(C()(T2()))*);
        
            template <typename, typename>
            static no& test(...);
        
            // If the "sizeof" the result of calling test<T>(0) would be equal to the     sizeof(yes),
            // the first overload worked and T has a nested type named foobar.
            static const bool value = sizeof(test<T>(0)) == sizeof(yes);
        };
        

        编辑 here 是一种修改,用于处理默认构造函数不可用的情况。

        【讨论】:

        • Re 这些假设默认构造函数对您检查的类型有效。不确定如何解决这个问题。看看std::declval
        • @jrok 谢谢,我还没有看到那个。在我附加的 pastebin 中,我只使用了一个定义了必要的转换运算符的辅助结构,但我想我可以用 declval 替换它。
        猜你喜欢
        • 2023-02-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-26
        相关资源
        最近更新 更多