26

基本上,我想要实现的是注册可调用(函数、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;
};
4

6 回答 6

15

您可以使用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!");

居住

于 2017-12-07T15:43:36.263 回答
13

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

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;
};
于 2017-12-07T16:13:32.450 回答
12

大多数答案都集中在基本回答这个问题上:你能用这些类型的值调用给定的函数对象吗?这与匹配签名不同,因为它允许您说您不想要的许多隐式转换。为了获得更严格的匹配,我们必须做一堆 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 限定方向错误的情况。

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

这是一个活生生的例子: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);
}
于 2017-12-07T16:53:52.270 回答
2

如果您接受A在可变参数模板类中进行转换,则可以使用decltype(),Register仅在callable兼容时才激活,如下所示

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 */ };
于 2017-12-07T15:46:25.107 回答
2

当您可以使用 C++17 时,这是 @R2RT 答案的另一个版本。我们可以使用 traitis_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仅适用于可调用的参数类型。

于 2021-03-12T18:58:25.353 回答
1

您可以使用检测成语,它是 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 库。

于 2017-12-07T16:03:48.527 回答