【问题标题】:How to check if template argument is a callable with a given signature如何检查模板参数是否是具有给定签名的可调用对象
【发布时间】:2018-05-21 18:23:43
【问题描述】:

基本上,我想要实现的是注册可调用(函数、lambda、带有调用运算符的结构)具有正确签名的编译时验证(可能带有很好的错误消息)。示例(static_assert的内容要填写):

struct A {
  using Signature = void(int, double);

  template <typename Callable>
  void Register(Callable &&callable) {
    static_assert(/* ... */);
    callback = callable;
  }

  std::function<Signature> callback;
};

【问题讨论】:

  • 您想要相同的签名还是兼容的签名?例如,如果您的签名是bool(),您会接受类型int(*)(),即指向不带参数并返回整数的函数的函数指针吗?整数将隐式转换为 bool,因此它是兼容的,但并不完全相同。
  • 这是一个很好的问题——我希望有尽可能强的类型,所以对于我的用例来说,在这种情况下最好拒绝 int(*)()。

标签: c++ c++11 templates template-meta-programming callable-object


【解决方案1】:

您可以使用std::is_convertible(C++11 起),例如

static_assert(std::is_convertible_v<Callable&&, std::function<Signature>>, "Wrong Signature!");

static_assert(std::is_convertible_v<decltype(callable), decltype(callback)>, "Wrong Signature!");

LIVE

【讨论】:

  • 这是一个很好的技术,但不清楚它是否正是 OP 想要的,std::function 我认为更宽松一点,只需要“兼容”签名而不是相同的签名。
  • 我真的很喜欢这个简单的方法,但是由于更严格的参数检查,我将选择 Nir ​​Friedman 的答案。
【解决方案2】:

在 C++17 中有一个 trait is_invocable&lt;Callable, Args...&gt;,它完全符合您的要求。与is_convertible&lt;std::function&lt;Signature&gt;,...&gt; 相比,它的优势在于您不必指定返回类型。 这听起来可能有点矫枉过正,但最近我遇到了不得不使用它的问题,正是我的包装函数从传递的 Callable 中推断出它的返回类型,但我已经传递了像 [](auto&amp; x){return 2*x;} 这样的模板化 lambda,所以它的返回类型在子调用。我无法将其转换为 std::function,最终我使用了 C++14 的 is_invocable 的本地实现。我找不到我从哪里得到它的链接......无论如何,代码:

template <class F, class... Args>
struct is_invocable
{
    template <class U>
    static auto test(U* p) -> decltype((*p)(std::declval<Args>()...), void(), std::true_type());
    template <class U>
    static auto test(...) -> decltype(std::false_type());

    static constexpr bool value = decltype(test<F>(0))::value;
};

对于你的例子:

struct A {
using Signature = void(int, double);

template <typename Callable>
void Register(Callable &&callable) {
    static_assert(is_invocable<Callable,int,double>::value, "not foo(int,double)");
    callback = callable;
}

std::function<Signature> callback;
};

【讨论】:

    【解决方案3】:

    大部分答案都集中在基本回答这个问题上:你能调用具有这些类型值的给定函数对象吗?这与匹配签名不同,因为它允许您说您不想要的许多隐式转换。为了获得更严格的匹配,我们必须做一堆 TMP。首先,这个答案:Call function with part of variadic arguments 展示了如何获取参数的确切类型和可调用的返回类型。代码转载在这里:

    template <typename T>
    struct function_traits : public function_traits<decltype(&T::operator())>
    {};
    
    template <typename ClassType, typename ReturnType, typename... Args>
    struct function_traits<ReturnType(ClassType::*)(Args...) const>
    {
        using result_type = ReturnType;
        using arg_tuple = std::tuple<Args...>;
        static constexpr auto arity = sizeof...(Args);
    };
    
    template <typename R, typename ... Args>
    struct function_traits<R(&)(Args...)>
    {
        using result_type = R;
        using arg_tuple = std::tuple<Args...>;
        static constexpr auto arity = sizeof...(Args);
    };
    

    完成后,您现在可以在代码中放置一系列静态断言:

    struct A {
      using Signature = void(int, double);
    
      template <typename Callable>
      void Register(Callable &&callable) {
        using ft = function_traits<Callable>;
        static_assert(std::is_same<int,
            std::decay_t<std::tuple_element_t<0, typename ft::arg_tuple>>>::value, "");
        static_assert(std::is_same<double,
            std::decay_t<std::tuple_element_t<1, typename ft::arg_tuple>>>::value, "");
        static_assert(std::is_same<void,
            std::decay_t<typename ft::result_type>>::value, "");
    
        callback = callable;
      }
    
      std::function<Signature> callback;
    };
    

    由于您是按值传递,这基本上就是您所需要的。如果您通过引用传递,我会在您使用其他答案之一的地方添加一个额外的静态断言;大概是宋元瑶的回答。这将处理例如基本类型相同但 const 限定方向错误的情况。

    当然,您可以在类型 Signature 上使这一切都通用,而不是像我所做的那样(简单地重复静态断言中的类型)。这会更好,但它会给已经很重要的答案添加更复杂的 TMP;如果你觉得你会在许多不同的Signatures 上使用它,或者它经常更改,那么可能也值得添加该代码。

    下面是一个活生生的例子:http://coliru.stacked-crooked.com/a/cee084dce9e8dc09。特别是我的例子:

    void foo(int, double) {}
    void foo2(double, double) {}
    
    int main()
    {
        A a;
        // compiles
        a.Register([] (int, double) {});
        // doesn't
        //a.Register([] (int, double) { return true; });
        // works
        a.Register(foo);
        // doesn't
        //a.Register(foo2);
    }
    

    【讨论】:

      【解决方案4】:

      如果您接受在可变参数模板类中转换A,则可以使用decltype(),只有在callable兼容的情况下才能激活Register,如下所示

      template <typename R, typename ... Args>
      struct A
       {
         using Signature = R(Args...);
      
         template <typename Callable>
         auto Register (Callable && callable)
            -> decltype( callable(std::declval<Args>()...), void() )
          { callback = callable; }
      
         std::function<Signature> callback;
       };
      

      这样,如果你愿意,用一个不兼容的函数调用Register(),你可以获得一个软错误并激活另一个Register()函数

      void Register (...)
       { /* do something else */ };
      

      【讨论】:

        【解决方案5】:

        当您可以使用 C++17 时,这是 @R2RT 答案的另一个版本。我们可以使用 trait is_invocable_r 来完成这项工作:

        struct Registry {
          std::function<void(int, double)> callback;
        
          template <typename Callable, 
              std::enable_if_t<
                  std::is_invocable_r_v<void, Callable, int, double>>* = nullptr>
          void Register(Callable callable) {
            callback = callable;
          }
        };
        
        int main() {
          Registry r;
          r.Register([](int a, double b) { std::cout << a + b << std::endl; });
          r.callback(35, 3.5);
        }
        

        打印出来 38.5

        std::is_invocable_r 的优点在于它允许您控制返回类型以及参数类型,而 std::is_invocable 仅适用于可调用的参数类型。

        【讨论】:

          【解决方案6】:

          您可以使用检测成语,它是 sfinae 的一种形式。我相信这适用于 c++11。

          template <typename...>
          using void_t = void;
          
          template <typename Callable, typename enable=void>
          struct callable_the_way_i_want : std::false_type {};
          
          template <typename Callable>
          struct callable_the_way_i_want <Callable, void_t <decltype (std::declval <Callable>()(int {},double {}))>> : std::true_type {};
          

          然后你可以像这样在你的代码中写一个静态断言:

          static_assert (is_callable_the_way_i_want <Callable>::value, "Not callable with required signature!");
          

          与我在上面看到的答案相比,这样做的优势是:

          • 它适用于任何可调用对象,而不仅仅是 lambda
          • 没有运行时开销或std::function 业务。 std::function 可能会导致动态分配,例如,这是不必要的。
          • 您实际上可以针对测试写一个 static_assert 并在其中放置一个很好的人类可读错误消息

          Tartan Llama 写了一篇关于这项技术的精彩博文,以及几种替代方法,看看吧! https://blog.tartanllama.xyz/detection-idiom/

          如果您经常需要这样做,那么您可能需要查看 callable_traits 库。

          【讨论】:

          • 当检测到 Callable 实际上需要两个 int 参数时,这不起作用,因为 double 可以隐式转换为 int,编译器会错误地接受 callable,尽管它不是精确的签名 void(*func)(整数,双)
          猜你喜欢
          • 1970-01-01
          • 2021-06-16
          • 1970-01-01
          • 1970-01-01
          • 2014-07-29
          • 1970-01-01
          • 2021-06-26
          • 2021-12-29
          相关资源
          最近更新 更多