文章目录
- 一、引言
- 二、`std::is_invocable` 概述
- 代码示例
- 输出结果
- 三、`std::is_invocable` 的工作原理
- 简化实现示例
- 四、`std::is_invocable` 的相关变体
- 1. `std::is_invocable_r`
- 2. `std::is_nothrow_invocable` 和 `std::is_nothrow_invocable_r`
- 五、使用场景
- 1. 模板元编程
- 2. 泛型算法
- 六、注意事项
- 七、结论
一、引言
在现代 C++ 编程中,我们经常会编写一些通用的代码,这些代码需要处理不同类型的可调用对象(如函数、函数指针、成员函数指针、lambda 表达式等)。在使用这些可调用对象之前,我们可能需要在编译时就确定它们是否可以以特定的参数列表进行调用。C++17 引入的 std::is_invocable
系列类型特征就为我们提供了这样的能力,它允许我们在编译时进行调用可行性的检查,从而增强代码的健壮性和通用性。
二、std::is_invocable
概述
std::is_invocable
是定义在 <type_traits>
头文件中的一个模板元函数。它用于在编译时检查一个可调用对象是否可以使用给定的参数类型进行调用。std::is_invocable
有多个重载形式,基本形式如下:
template< class F, class... Args >
struct is_invocable;
template< class F, class... Args >
inline constexpr bool is_invocable_v = is_invocable<F, Args...>::value;
代码示例
#include <iostream>
#include <type_traits>
// 普通函数
void foo(int x) {
std::cout << "foo called with " << x << std::endl;
}
int main() {
std::cout << std::boolalpha;
// 检查 foo 是否可以用 int 类型参数调用
std::cout << "Is foo invocable with int? " << std::is_invocable_v<decltype(foo), int> << std::endl;
// 检查 foo 是否可以用 double 类型参数调用(隐式转换可行)
std::cout << "Is foo invocable with double? " << std::is_invocable_v<decltype(foo), double> << std::endl;
return 0;
}
输出结果
Is foo invocable with int? true
Is foo invocable with double? true
在上述代码中,我们定义了一个普通函数 foo
,它接受一个 int
类型的参数。然后使用 std::is_invocable_v
检查 foo
是否可以用 int
和 double
类型的参数调用。由于 double
可以隐式转换为 int
,所以两种检查结果都为 true
。
三、std::is_invocable
的工作原理
std::is_invocable
的实现基于 SFINAE(Substitution Failure Is Not An Error)原则。当我们使用 std::is_invocable<F, Args...>
时,编译器会尝试在编译时构造一个对可调用对象 F
的调用,参数类型为 Args...
。如果这个调用是合法的,那么 std::is_invocable<F, Args...>::value
将为 true
;否则,它将为 false
。
简化实现示例
#include <type_traits>
// 辅助模板,用于检测调用是否可行
template <typename F, typename... Args, typename = void>
struct is_invocable_helper : std::false_type {};
template <typename F, typename... Args>
struct is_invocable_helper<F, Args..., std::void_t<decltype(std::declval<F>()(std::declval<Args>()...))>>
: std::true_type {};
// 定义 is_invocable
template <typename F, typename... Args>
struct is_invocable : is_invocable_helper<F, Args...> {};
// 辅助模板,用于打印结果
template <typename F, typename... Args>
void print_is_invocable() {
std::cout << "Is callable with given args? " << is_invocable<F, Args...>::value << std::endl;
}
// 普通函数
void bar(int x) {}
int main() {
std::cout << std::boolalpha;
print_is_invocable<decltype(bar), int>();
return 0;
}
在这个示例中,我们定义了一个辅助模板 is_invocable_helper
,它使用 std::void_t
和 decltype
来检测对可调用对象 F
的调用是否合法。如果合法,is_invocable_helper
将继承自 std::true_type
;否则,它将继承自 std::false_type
。
四、std::is_invocable
的相关变体
1. std::is_invocable_r
std::is_invocable_r
用于检查一个可调用对象是否可以使用给定的参数类型进行调用,并且返回值可以隐式转换为指定的类型。
#include <iostream>
#include <type_traits>
int add(int a, int b) {
return a + b;
}
int main() {
std::cout << std::boolalpha;
// 检查 add 是否可以用 int, int 调用并返回 int
std::cout << "Is add invocable with int, int and return int? " << std::is_invocable_r_v<int, decltype(add), int, int> << std::endl;
// 检查 add 是否可以用 int, int 调用并返回 double
std::cout << "Is add invocable with int, int and return double? " << std::is_invocable_r_v<double, decltype(add), int, int> << std::endl;
return 0;
}
2. std::is_nothrow_invocable
和 std::is_nothrow_invocable_r
std::is_nothrow_invocable
检查一个可调用对象是否可以使用给定的参数类型进行调用,并且调用过程不会抛出异常。std::is_nothrow_invocable_r
则在此基础上还要求返回值可以隐式转换为指定的类型。
#include <iostream>
#include <type_traits>
// 不抛出异常的函数
void safe_foo(int x) noexcept {
std::cout << "safe_foo called with " << x << std::endl;
}
int main() {
std::cout << std::boolalpha;
// 检查 safe_foo 是否可以用 int 调用且不抛出异常
std::cout << "Is safe_foo nothrow invocable with int? " << std::is_nothrow_invocable_v<decltype(safe_foo), int> << std::endl;
return 0;
}
五、使用场景
1. 模板元编程
在模板元编程中,我们经常需要根据可调用对象的调用可行性来选择不同的实现路径。
#include <iostream>
#include <type_traits>
template <typename F, typename... Args, std::enable_if_t<std::is_invocable_v<F, Args...>, int> = 0>
auto call_if_invocable(F&& f, Args&&... args) {
return std::forward<F>(f)(std::forward<Args>(args)...);
}
template <typename F, typename... Args, std::enable_if_t<!std::is_invocable_v<F, Args...>, int> = 0>
void call_if_invocable(F&&, Args&&...) {
std::cout << "Not invocable." << std::endl;
}
void baz(int x) {
std::cout << "baz called with " << x << std::endl;
}
int main() {
call_if_invocable(baz, 42);
call_if_invocable([](double) {}, 10); // 这里不匹配调用,输出 Not invocable.
return 0;
}
2. 泛型算法
在编写泛型算法时,我们可以使用 std::is_invocable
来确保传入的可调用对象符合算法的要求。
#include <iostream>
#include <vector>
#include <type_traits>
template <typename Container, typename Func, std::enable_if_t<std::is_invocable_v<Func, typename Container::value_type>, int> = 0>
void apply(Container& c, Func f) {
for (auto& elem : c) {
f(elem);
}
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
auto print = [](int x) { std::cout << x << " "; };
apply(numbers, print);
std::cout << std::endl;
return 0;
}
六、注意事项
- 隐式转换:
std::is_invocable
会考虑参数的隐式转换。例如,如果一个函数接受int
类型的参数,那么传入short
或char
类型的参数也会被认为是可调用的,因为存在隐式转换。 - 成员函数指针:在使用成员函数指针时,需要注意传递合适的对象实例作为第一个参数。例如,对于一个成员函数
void MyClass::func()
,调用时需要传递MyClass
的实例或指针。
#include <iostream>
#include <type_traits>
class MyClass {
public:
void member_func() {
std::cout << "Member function called." << std::endl;
}
};
int main() {
std::cout << std::boolalpha;
// 检查成员函数指针是否可调用
std::cout << "Is member_func invocable? " << std::is_invocable_v<decltype(&MyClass::member_func), MyClass&> << std::endl;
return 0;
}
七、结论
std::is_invocable
系列类型特征为 C++ 程序员提供了强大的编译时检查能力,使得我们可以在编写通用代码时更加安全和高效。通过合理使用 std::is_invocable
及其变体,我们可以避免在运行时出现调用错误,提高代码的健壮性和可维护性。同时,在模板元编程和泛型算法中,std::is_invocable
也发挥着重要的作用。