编程语言的发展日益重视安全性,对于C++而言,其本身存在诸多需要关注的细节。在常规调用场景下,若调用函数(包括回调函数)时传递的参数在类型或数量上不匹配,极易引发问题。如果是显式调用,问题相对容易定位;但在模板编程中进行动态函数调用时,问题可能直到运行时才暴露,甚至直接导致程序崩溃。因此,在调用前进行验证(类似于合规性检查)能够有效防范此类风险。
在前文探讨中,我们了解到可以使用SFINAE技术进行函数参数检查,但其复杂的调试过程让许多开发者望而却步。为此,C++17标准库提供了元编程接口std::is_invocable及其系列工具,使用标准接口的优势显而易见:不仅降低了开发者的编程复杂度,更重要的是显著提升了代码的可移植性。
一、std::is_invocable 说明
std::is_invocable 通过验证可调用对象(函数、函数指针、Lambda表达式等)与给定参数列表是否匹配,来确保函数调用的安全性。它在编译期完成检查,是编写健壮模板代码的重要工具。
二、C++ 标准库中的定义
std::is_invocable 系列工具定义在 <type_traits> 头文件中。具体声明如下:
// 1. 判断 Fn 对象使用 ArgTypes... 参数调用是否格式正确
template< class Fn, class... ArgTypes >
struct is_invocable;
// 2. 判断 Fn 对象使用 ArgTypes... 参数调用是否格式正确,且返回值可转换为 R
template< class R, class Fn, class... ArgTypes >
struct is_invocable_r;
// 3. 同 1,且要求调用过程为 noexcept(不抛出异常)
template< class Fn, class... ArgTypes >
struct is_nothrow_invocable;
// 4. 同 2,且要求调用过程为 noexcept
template< class R, class Fn, class... ArgTypes >
struct is_nothrow_invocable_r;
// 对应的辅助变量模板 (C++17)
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;
简而言之,基础接口is_invocable仅检查调用格式,而is_invocable_r额外检查返回值转换,带nothrow的版本则进一步要求调用操作不会抛出异常。
其核心实现代码(示例,可能因编译器而异)仍然依赖于 SFINAE 技术:
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");
};
// ... 更多底层实现细节
这段代码看起来有些熟悉,它与我们在“SFINAE的技巧应用”中讨论的第一个例子在思路上有相似之处。不过需注意,不同编译器的实现可能存在细节差异。
三、技术原理分析
std::is_invocable 的内部实现依然紧密依托于 SFINAE 技术。当编译器处理 std::is_invocable<Fn, Args...> 时,会在编译期尝试构造一个对可调用对象 Fn 使用参数 Args... 的调用表达式。如果该表达式合法,则 value 成员为 true;否则,SFINAE 规则使其推导失败,value 为 false。变参模板的使用也自然涉及到了引用折叠和完美转发等机制。
理解 decltype 和 declval 的用法对于剖析其原理至关重要。特别是 declval,它可以在不创建实际实例的情况下获取类型的引用,两者配合能够巧妙地实现对参数和返回类型的编译期检测,这是在模板元编程和 SFINAE 中非常普遍的技术。
四、应用场景与注意事项
结合其说明和源码可知,std::is_invocable 的主要应用场景集中于模板编程,尤其是元编程领域:
- 编译期安全检查:对函数签名、回调接口进行合规性验证。
- 异常安全控制:利用
nothrow 版本确保某些关键调用不会抛出异常。
- 泛型编程中的约束:在编写模板时,检查传入的可调用对象是否满足特定调用约定。
使用中需要注意以下几点:
- 该系列工具会考虑隐式类型转换。
- 处理成员函数指针时,需注意普通成员函数与静态成员函数的调用方式差异(前者需要对象实例作为参数)。
- 使用
nothrow 相关接口时,需确保调用确实标记为或不会抛出异常。
五、代码示例
以下是一个简单的使用示例:
#include <iostream>
#include <type_traits>
class Demo {
public:
int checkFunc(int d) { return d; }
static int staticCheckFunc(int d) { return d * d; }
};
void test() {
// 检查非静态成员函数
bool b1 = std::is_invocable_r<int, decltype(&Demo::checkFunc), Demo*, int>::value;
std::cout << "checkFunc (Demo*, int) result:" << b1 << std::endl; // true
bool b2 = std::is_invocable_r<int, decltype(&Demo::checkFunc), Demo&, int>::value;
std::cout << "checkFunc (Demo&, int) result:" << b2 << std::endl; // true
// 检查静态成员函数
bool b3 = std::is_invocable_r<int, decltype(&Demo::staticCheckFunc), int>::value;
std::cout << "staticCheckFunc (int) result:" << b3 << std::endl; // true
}
auto func2(char) -> int (*)() {
return nullptr;
}
int main() {
test();
// 使用 static_assert 和辅助变量模板进行编译期断言
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>);
return 0;
}
此类接口的用法相对直观,重点在于理解如何对不同类型的可调用对象(特别是类成员函数)进行调用格式的检测。
六、总结
std::is_invocable 系列接口是 C++17 为函数调用参数检查提供的标准化方案。它提供了强大的编译期安全机制,能够有效降低开发复杂度和潜在的运行时错误,显著提升了模板元编程代码的健壮性与可靠性。