c++ 如何检查模板参数是否是具有给定签名的可调用对象

to94eoyn  于 2023-01-18  发布在  其他
关注(0)|答案(7)|浏览(180)

基本上,我想要实现的是编译时验证(可能会有漂亮的错误消息)注册的可调用对象(函数、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;
};
jum4pzuy

jum4pzuy1#

您可以使用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

ctzwtxfj

ctzwtxfj2#

C++20(含概念)

几年前,我们就有了一些概念,可以让这样的检查变得容易。这个例子是std::invocable的直接使用。

#include <concepts>

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

template <typename Callable>
void Register(Callable &&callable) 
    requires std::invocable<Callable, int, double>
{
    callback = callable;
}

std::function<Signature> callback;
};

C++17语言

在C17中有trait std::is_invocable<Callable, Args...>,它能完全满足你的要求,它比std::is_convertible<std::function<Signature>,...>的优点是你不必指定返回类型。
这听起来可能有点过头了,但我遇到了不得不使用它的问题。确切地说,我的 Package 器函数从传递的Callable推导出了它的返回类型,但我传递了像[](auto& x){return 2*x;}这样的模板化lambda,所以它的返回类型是在subcall中推导出来的。我不能将它转换为std::function,最后我使用了is_invocable的C
14本地实现。

C++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;
};
qyswt5oh

qyswt5oh3#

大部分的答案都集中在基本回答这个问题上:你能用这些类型的值调用给定的函数对象吗?这和匹配签名不一样,因为它允许很多你说你不想要的隐式转换。为了得到更严格的匹配,我们必须做很多TMP。首先,这个答案:带部分可变参数的Call函数演示了如何获取参数的确切类型和可调用对象的返回类型。代码如下所示:

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);
};

完成这些操作后,现在可以在代码中放置一系列静态Assert:

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;
};

因为你是通过值传递的,所以这基本上就是你所需要的。如果你是通过引用传递的,我会添加一个额外的静态Assert来使用其他的答案之一;可能是songyuoyao的答案。这将处理一些情况,例如基类型是相同的,但是const限定符的方向错误。
当然,你也可以让这些都通用于Signature类型,而不是像我一样(简单地重复静态Assert中的类型),这样做会更好,但是它会给一个已经很重要的答案增加更复杂的TMP;如果您觉得您将在许多不同的Signature中使用它,或者它经常更改,那么可能也值得添加这些代码。
下面是一个活生生的例子: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);
}
brccelvz

brccelvz4#

如果您接受在可变参数模板类中转换A,则仅当callable兼容时,才可以使用decltype()激活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 */ };
y1aodyip

y1aodyip5#

当你能使用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仅用于可调用的参数类型。

xt0899hw

xt0899hw6#

你可以使用检测习语,它是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 {};

然后,您可以在代码中编写一个静态Assert,如下所示:

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

与我上面看到的答案相比,这种方法的优点是:

  • 它适用于任何可调用对象,而不仅仅是lambda
  • 不存在运行时开销或X1 M0 N1 X业务。X1 M1 N1 X可导致例如动态分配,否则这将是不必要的。
  • 实际上,您可以针对测试编写一个static_assert,并在其中放置一个很好的人类可读的错误消息

Tartan Llama写了一篇关于这种技术的很棒的博客,还有几种替代方法,看看吧!https://blog.tartanllama.xyz/detection-idiom/
如果你需要做很多这样的事情,那么你可能想看看callable_traits库。

jvlzgdj9

jvlzgdj97#

基于@尼尔·弗里德曼的回答。

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 signature = ::std::tuple<ReturnType, ClassType, Args...>;
};

template<typename ReturnType, typename ... Args>
struct function_traits<ReturnType(&)(Args...)> {
    using signature = ::std::tuple<ReturnType, void, Args...>;
};

template<typename T> using func_sig = typename function_traits<T>::signature;

template<typename Callable1, typename Callable2>
struct has_same_signature {
    static constexpr bool value = ::std::is_same<func_sig<Callable1>, func_sig<Callable2>>::value;
};

然后你可以做一些严格的检查

int f(int) {
}

int g(double) {
}

template<typename T>
void some_func(T&& a) {
    static_assert(has_same_signature<T, int(&)(int)>::value)
}

int main() {
    some_func(f); // success
    some_func(g); // fail
}

相关问题