如何检查模板参数是否是具有给定签名的可调用对象
How to check if template argument is a callable with a given signature
基本上,我想要实现的是编译时验证(可能带有很好的错误消息)注册的可调用对象(函数、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;
};
您可以使用 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!");
如果您接受在可变参数模板 class 中转换 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 */ };
您可以使用 detection 成语,它是 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 库。
在 C++17 中有特征 is_invocable<Callable, Args...>
,它完全满足您的要求。它优于 is_convertible<std::function<Signature>,...>
的优点是您不必指定 return 类型。
这听起来可能有点矫枉过正,但最近我遇到了必须使用它的问题,正是我的包装函数从传递的 Callable 中推导出它的 return 类型,但我已经传递了像这样的模板化 lambda [](auto& x){return 2*x;}
,所以 return 它的类型是在子调用中推导出来的。我无法将其转换为 std::function
,最终我使用了 is_invocable
的本地实现来实现 C++14。我找不到 link 我从哪里得到它...无论如何,代码:
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;
};
大部分答案都集中在基本回答问题上:你能用这些类型的值调用给定的函数对象吗?这与匹配签名不同,因为它允许许多你说你不想要的隐式转换。为了获得更严格的匹配,我们必须做一堆 TMP。首先,这个答案: 展示了如何获取参数的确切类型和可调用对象的 return 类型。代码转载于此:
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;如果您觉得您将把它与许多不同的 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);
}
当您可以使用 C++17 时,这是 @R2RT 的另一个版本的答案。我们可以使用特征 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
的优点在于它允许您控制 return 类型和参数类型,而 std::is_invocable
仅控制可调用参数类型。
基本上,我想要实现的是编译时验证(可能带有很好的错误消息)注册的可调用对象(函数、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;
};
您可以使用 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!");
如果您接受在可变参数模板 class 中转换 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 */ };
您可以使用 detection 成语,它是 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 库。
在 C++17 中有特征 is_invocable<Callable, Args...>
,它完全满足您的要求。它优于 is_convertible<std::function<Signature>,...>
的优点是您不必指定 return 类型。
这听起来可能有点矫枉过正,但最近我遇到了必须使用它的问题,正是我的包装函数从传递的 Callable 中推导出它的 return 类型,但我已经传递了像这样的模板化 lambda [](auto& x){return 2*x;}
,所以 return 它的类型是在子调用中推导出来的。我无法将其转换为 std::function
,最终我使用了 is_invocable
的本地实现来实现 C++14。我找不到 link 我从哪里得到它...无论如何,代码:
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;
};
大部分答案都集中在基本回答问题上:你能用这些类型的值调用给定的函数对象吗?这与匹配签名不同,因为它允许许多你说你不想要的隐式转换。为了获得更严格的匹配,我们必须做一堆 TMP。首先,这个答案:
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;如果您觉得您将把它与许多不同的 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);
}
当您可以使用 C++17 时,这是 @R2RT 的另一个版本的答案。我们可以使用特征 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
的优点在于它允许您控制 return 类型和参数类型,而 std::is_invocable
仅控制可调用参数类型。