一、std::is_invocable说明
编程语言的发展到现在,安全性的问题已经成为了重中之重的问题。而对C++语言来说,本身就存在着非常多的细节的安全问题。在经常应用的场景中,如果调用一个函数(包括回调函数),传递的参数(类型、数量)有问题的话,极有可能产生问题。如果单纯是显式的调用还好理解,如果是在模板编程中动态调用函数时,有可能会到运行时才会发现问题。而此时的结果可能就是崩溃了。所以,在调用前进行验证(类似合规性检查),则可以防范不少的风险。
在前面的学习中,大家了解可以用SFINAE技术来进行函数的参数检查,但SFINAE的复杂的调试难度让开发者有些难以接受。所以在C++17标准中,提供了元编程接口std::is_invocable及其系列接口。使用标准的接口优势非常明显,既让开发者的编程复杂度降低,但更重要的是让代码的移植性显著的提高。
二、C++中的定义
C++17中的std::is_invocable是通过验证可调用函数对象对应的函数参数是否合规来确定函数对象的调用的安全性。对std::is_invocable系列的定义在<type_traits>头文件中,具体的声明如下:
c
//1、判断Fn对象(函数、函数指针、Lambda表达式等)函数调用参数是否格式正确
template< class Fn, class... ArgTypes >
struct is_invocable;
//2、判断Fn函数对象调用参数是否格式正确,并且返回值可以隐式转换为指定类型R
template< class R, class Fn, class... ArgTypes >
struct is_invocable_r;
//3、同上1且不抛出异常
template< class Fn, class... ArgTypes >
struct is_nothrow_invocable;
//4、同上2且不抛出异常
template< class R, class Fn, class... ArgTypes >
struct is_nothrow_invocable_r;
//辅助函数
template< class Fn, class... ArgTypes >
inline constexpr bool is_invocable_v =
std::is_invocable<Fn, ArgTypes...>::value;
template< class R, class Fn, class... ArgTypes >
inline constexpr bool is_invocable_r_v =
std::is_invocable_r<R, Fn, ArgTypes...>::value;
template< class Fn, class... ArgTypes >
inline constexpr bool is_nothrow_invocable_v =
std::is_nothrow_invocable<Fn, ArgTypes...>::value;
template< class R, class Fn, class... ArgTypes >
inline constexpr bool is_nothrow_invocable_r_v =
std::is_nothrow_invocable_r<R, Fn, ArgTypes...>::value;
其实说的简单点就是在提供基础的调用参数的检查的检查上,后面的接口增加了对返回值、异常抛出的检查,这也符合人们的认知习惯。
其实现的具体代码如下:
c
template<typename _Fn, typename... _ArgTypes>
struct __is_invocable
: __is_invocable_impl<__invoke_result<_Fn, _ArgTypes...>, void>::type
{ };
template<typename _Fn, typename... _ArgTypes>
struct is_invocable
: __is_invocable_impl<__invoke_result<_Fn, _ArgTypes...>, void>::type
{
static_assert(std::__is_complete_or_unbounded(__type_identity<_Fn>{}),
"_Fn must be a complete class or an unbounded array");
static_assert((std::__is_complete_or_unbounded(
__type_identity<_ArgTypes>{}) && ...),
"each argument type must be a complete class or an unbounded array");
};
template<typename _Result, typename _Ret,
bool = is_void<_Ret>::value, typename = void>
struct __is_invocable_impl
: false_type
{
using __nothrow_type = false_type; // For is_nothrow_invocable_r
};
// Used for valid INVOKE and INVOKE<void> expressions.
template<typename _Result, typename _Ret>
struct __is_invocable_impl<_Result, _Ret,
/* is_void<_Ret> = */ true,
__void_t<typename _Result::type>>
: true_type
{
using __nothrow_type = true_type; // For is_nothrow_invocable_r
};
template<typename _Result, typename _Ret>
struct __is_invocable_impl<_Result, _Ret,
/* is_void<_Ret> = */ false,
__void_t<typename _Result::type>>
{
private:
// The type of the INVOKE expression.
// Unlike declval, this doesn't add_rvalue_reference, so it respects
// guaranteed copy elision.
static typename _Result::type _S_get() noexcept;
template<typename _Tp>
static void _S_conv(_Tp) noexcept;
// This overload is viable if INVOKE(f, args...) can convert to _Tp.
template<typename _Tp, bool _Check_Noex = false,
typename = decltype(_S_conv<_Tp>(_S_get())),
bool _Noex = noexcept(_S_conv<_Tp>(_S_get()))>
static __bool_constant<_Check_Noex ? _Noex : true>
_S_test(int);
template<typename _Tp, bool = false>
static false_type
_S_test(...);
public:
// For is_invocable_r
using type = decltype(_S_test<_Ret>(1));
// For is_nothrow_invocable_r
using __nothrow_type = decltype(_S_test<_Ret, true>(1));
};
上面的代码看起来有点熟悉,在前面"SFINAE的技巧应用"中的第一个例子是不是有些类似?不过,不同的编译器中可能实现有细节上的不同,大家在看代码时需要明白这一点。
三、技术分析
std::is_invocable的内部实现仍然与SFINAE技术紧密相关,也就是说在调用 std::is_invocable<Fn, Args...> 时,编译器会尝试在编译时构造一个对可调用函数对象Fn的调用,同时对变参 Args...进行处理。如果此调用检测通过,则std::is_invocable<Fn, Args...>::value将为 true;否则,将为 false。当然,变参的存在,使得这其中肯定有引用折叠和完美转发的情况。这也正好是上面的实现代码的部分。
在前面的学习中,大家已经明白了decltype和declval的用法,特别是后者,可以不需要创建一个真正的实例来获取相关的类型。二者配合就可以实现参数和返回类型的检测。它们两个在SFINAE中的应用是十分普遍的。更详细的可以分析一下具体的实现代码就会明白其中的道理。
四、应用场景和限制
其实看到它的说明和源码就会明白,它的主要应用场景就是在模板编程中,特别是元编程。主要包括:
- 编译时代码安全的控制检查,如函数的签名检查
- 对异常安全的控制,这也是SFINAE本身的特点
- 模板编程和元编程中的可调用对象检查
需要注意的是,std::is_invocable系列中,会自动隐式的处理参数的转换。另外在处理函数指针时,要注意普通函数指针和类成员函数指针的具体的应用方法。在使用异常相关的接口时还要保证操作不抛出异常。
五、例程
具体的用法如下:
c
#include <iostream>
#include <type_traits>
class Demo {
public:
int checkFunc(int d) { return d; }
static int staticCheckFunc(int d) { return d * d; }
};
void test() {
// check non-static member function
bool b1 = std::is_invocable_r<int, decltype(&Demo::checkFunc), Demo *, int>::value;
std::cout << "checkFunc check result:" << b1 << std::endl;
bool b2 = std::is_invocable_r<int, decltype(&Demo::checkFunc), Demo &, int>::value;
std::cout << "checkFunc check result:" << b2 << std::endl;
// check static member function
bool b3 = std::is_invocable_r<int, decltype(&Demo::staticCheckFunc), int>::value;
std::cout << "staticCheckFunc check result:" << b3 << std::endl;
}
auto func2(char) -> int (*)()
{
return nullptr;
}
int main()
{
test();
static_assert(std::is_invocable_v<int()>);
static_assert(not std::is_invocable_v<int(), int>);
static_assert(std::is_invocable_r_v<int, int()>);
static_assert(not std::is_invocable_r_v<int*, int()>);
static_assert(std::is_invocable_r_v<void, void(int), int>);
static_assert(not std::is_invocable_r_v<void, void(int), void>);
static_assert(std::is_invocable_r_v<int(*)(), decltype(func2), char>);
static_assert(not std::is_invocable_r_v<int(*)(), decltype(func2), void>);
}
这种接口的代码没有什么可分析的,大家看看就明白了。重点看看函数内部函数的调用检测的情况。
六、总结
std::is_invocable系列接口是C++17库中统一函数调用参数检查的标准方式。它提供了在编译期的安全检查的机制,降低了开发的复杂度,提高了代码开发的效率。在模板编程特别是元编程中,可以有效的提高代码的安全性和健壮性。