深入探究 C++17 std::is_invocable

news/2025/2/9 6:19:58 标签: c++, 开发语言

在这里插入图片描述

文章目录

    • 一、引言
    • 二、`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 是否可以用 intdouble 类型的参数调用。由于 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_tdecltype 来检测对可调用对象 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_invocablestd::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 类型的参数,那么传入 shortchar 类型的参数也会被认为是可调用的,因为存在隐式转换。
  • 成员函数指针:在使用成员函数指针时,需要注意传递合适的对象实例作为第一个参数。例如,对于一个成员函数 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 也发挥着重要的作用。


http://www.niftyadmin.cn/n/5845690.html

相关文章

陶氏环面包络减速机:为工业视觉检测注入“精准动力”!

在工业4.0时代&#xff0c;视觉检测技术已成为智能制造的核心环节。无论是精密电子元件的检测&#xff0c;还是汽车零部件的质量把控&#xff0c;视觉检测系统都需要极高的精度、稳定性和响应速度。而这一切&#xff0c;离不开一颗强大的“心脏”——陶氏环面包络减速机。 一、…

linux安装oracle19c

安装 安装前检查配置&#xff1a; 挂载50g盘&#xff1a; vgcreate oravg /dev/sdb lvcreate -L 49.8G -n oralv oravg lvscan mkfs.xfs /dev/oravg/oralv 查看uuid blkid 复制分区表 cp /etc/fstab /etc/fstab.bakvi /etc/fstab内容为: /dev/oravg/oralv /u01 xfs defau…

Docker 容器 Elasticsearch 启动失败完整排查记录

背景 在服务器上运行 Docker 容器 es3&#xff0c;但 Elasticsearch 无法正常启动&#xff0c;运行 docker ps -a 发现 es3 处于 Exited (1) 状态&#xff0c;即进程异常退出。 本次排查从错误日志、容器挂载、权限问题、SELinux 影响、内核参数等多个方面入手&#xff0c;最…

webview_flutter的使用

目录 步骤示例代码 步骤 1.配置依赖。根目录下运行如下命令&#xff1a; flutter pub add webview_flutter 2.所需页面导入&#xff1a; import ‘package:webview_flutter/webview_flutter.dart’; 3.初始化WebViewController overridevoid initState() {super.initState();…

Verilog代码实例

Verilog语言学习&#xff01; 文章目录 目录 文章目录 前言 一、基本逻辑门代码设计和仿真 1.1 反相器 1.2 与非门 1.3 四位与非门 二、组合逻辑代码设计和仿真 2.1 二选一逻辑 2.2 case语句实现多路选择逻辑 2.3 补码转换 2.4 7段数码管译码器 三、时序逻辑代码设计和仿真 3.1…

k8s常见面试题1

k8s常见面试题1 Kubernetes 基础知识核心组件及作用K8S生成pod过程Pod、Deployment、Service的区别保证Pod高可用ConfigMap vs Secret网络模型与Pod通信Ingress 与 Service 的主要区别什么是 Ingressk8s中为什么要做ServiceIngress 与 Service 的区别 资源配额 Kubernetes 实践…

Golang:精通sync/atomic 包的Atomic 操作

在本指南中&#xff0c;我们将探索sync/atomic包的细节&#xff0c;展示如何编写更安全、更高效的并发代码。无论你是经验丰富的Gopher还是刚刚起步&#xff0c;你都会发现有价值的见解来提升Go编程技能。让我们一起开启原子运算的力量吧&#xff01; 理解Go中的原子操作 在快…

25/2/8 <机器人基础> 阻抗控制

1. 什么是阻抗控制&#xff1f; 阻抗控制旨在通过调节机器人与环境的相互作用&#xff0c;控制其动态行为。阻抗可以理解为一个力和位移之间的关系&#xff0c;涉及力、速度和位置的协同控制。 2. 阻抗控制的基本概念 力控制&#xff1a;根据感测的外力调节机械手的动作。位置…