std::invoke_result的实现详解
目录
前言
本篇博文将详细介绍一下libstdc++
中std::invoke_result
的实现过程,由于个人水平不足,可能最终的实现过程略有误差,还请各位指正。
invoke_result
std::invoke_result
是C++17标准库里面的一个接口,它可以返回任何可调用的返回值,比如函数,函数对象,对象方法等。它的接口为[1]
template< class F, class... ArgTypes>
class invoke_result;
在C++17之前,std::invoke_result
有一个类似功能的模板,名字为std::result_of
,他们之间的差别只有在接口上有略微的差别,
template< class >
class result_of; // not defined
template< class F, class... ArgTypes >
class result_of<F(ArgTypes...)>;
std::result_of
于C++17标记为deprecated
,并在C++20中被移除。
标准库中的invoke_result
我们首先看看一下标准库中std::invoke_result
的实现,了解一下其大致的结构。
invoke_result[std::invoke_result];
result_of[std::result_of];
__invoke_result[std::__invoke_result];
invoke_result --> __invoke_result;
result_of --> __invoke_result;
__result_of_impl[std::__result_of_impl];
__invoke_result --> __result_of_impl;
__result_of_memobj[std::__result_of_memobj];
__result_of_memfun[std::__result_of_memfun];
__result_of_other_impl[std::__result_of_other_impl];
__result_of_impl --> __result_of_memobj;
__result_of_impl --> __result_of_memfun;
__result_of_impl --> __result_of_other_impl;
__S_test["__S_test()"];
__result_of_other_impl --> __S_test;
__result_of_memobj_ref[std::__result_of_memobj_ref];
__result_of_memobj_deref[std::__result_of_memobj_deref];
__result_of_memobj --> __result_of_memobj_ref;
__result_of_memobj --> __result_of_memobj_deref;
__result_of_memfun_ref[std::__result_of_memfun_deref];
__result_of_memfun_deref[std::__result_of_memfun_deref];
__result_of_memfun --> __result_of_memfun_ref;
__result_of_memfun --> __result_of_memfun_deref;
__result_of_memfun_ref_impl[std::__result_of_memfun_ref_impl];
__result_of_memfun_deref_impl[std::__result_of_memfun_deref_impl];
__result_of_memobj_ref_impl[std::__result_of_memobj_ref_impl];
__result_of_memobj_deref_impl[std::__result_of_memobj_deref_impl];
__result_of_memfun_ref --> __result_of_memfun_ref_impl;
__result_of_memfun_deref --> __result_of_memfun_deref_impl;
__result_of_memobj_ref --> __result_of_memobj_ref_impl;
__result_of_memobj_deref --> __result_of_memobj_deref_impl;
__result_of_memfun_ref_impl --> __S_test;
__result_of_memfun_deref_impl --> __S_test;
__result_of_memobj_ref_impl --> __S_test;
__result_of_memobj_deref_impl --> __S_test;
可以看到,整个实现有两层判断(有两个判断是类似的,可以看作是一层),其中第一个层位于std::__result_of_impl
,用来判断不同的调用方法,大致分为三种:
- 对象方法,比如以下代码中的
struct A
的invoke_mem_fun
,调用方法为obj.fun(args...)
。
struct A {
int invoke_mem_fun();
};
- 对象成员,比如以下代码中的
struct B
中的invoke_mem_obj
,调用方法为obj.fun
(其实就是一个简单的引用成员)。
struct B {
int invoke_mem_obj;
};
- 其他,比如以下代码中的
invoke_other
,其调用方法为fun(args...)
,这一部分包括了函数对象和普通函数。
int invoke_other();
那么std::__result_of_impl
是如何区分出了这三种不同的调用方式呢?
libstdc++
中实现了两个类型判断,is_member_object_pointer
和is_member_function_pointer
。通过这两个判断不同的输出,来区分三种不同的调用方式。
这两个函数的结构差不多,首先会对判断输入的类型是否为对象的成员,判断方法为进行类似下面代码的模式匹配
template<typename>
struct __mem_and_obj_type: public std::__failure_type{};
template<typename _Tp, typename _Cp>
struct __mem_and_obj_type<_Tp _Cp::*> {
typedef _Tp mem_type;
typedef _Cp obj_type;
};
这一部分代码是从我实现的部分截取出来的,其中最主要的部分就是_Tp _Cp::*
,他分开成员类型和对象类型,如果能分开则为对象的成员。
第二步则是判断上一步分离得到的成员类型是否为函数类型,如果是函数类型,那么其为对象方法,否则为对象成员。
由此,通过那两个类型的判断,可以有三种不同的结果,true,false
,false,true
,false,false
,不同的结果对应着不同的调用方法。看到这里,肯定会有人思考,有没有可能结果都是true
呢?
首先从libstdc++
实现上这是不可能的,因为在实现第二步的时候,is_member_object_pointer
几乎是直接对is_member_function_pointer
取反的(实现上取反)。所以,不可能同时出现两个都为true
的情况。
而实际中,却的确有可能出现这种情况(我也不知道是不是我那里弄错了,先写下来吧)
struct STest {
double operator() (int, double) {
std::cout << "Test" << std::endl;
return 0;
}
};
struct CTest {
struct STest s;
int s_m(int, double){};
};
这种情况下,如果我们想获得CTest::s
的返回值,是有一个很奇怪的情况的,我们是把它当作函数呢,还是一个成员呢?如果从调用的情况来看,CTest::s
和CTest::s_m
一样,都是obj.fun(args...)
,但是实际中,std::invoke_result
会将它视为对象成员。所以如果我们使用std::invoke_result<decltype(&CTest::s), CTest, int, double>::type
是会报错的,正确的方法是只能使用std::invoke_result<decltype(&CTest::s), CTest>::type
。由于没有去查看文献,所以不清楚这一部分是bug还是feature或者是UB,有时间再详细考究一下吧。
好了,目前已经详细说完了std::invoke_result
的第一层判断,接下来我们来看看第二层判断。
第二层判断有两个部分,我们只看其中的一个部分,因为实际上这两个部分都是一样的。
这一层的判断位于对象成员和对象方法部分。即区分std::__result_of_memfun_deref
和std::__result_of_memfun_ref
。这两个的不同之处在于,前者的对象为指针。区分的方法也很简单粗暴,直接判断是否参数里面的对象类型与上文中使用_Tp _Cp::*
获得的对象类型_Cp
是否一致或者为继承关系,如果为是,则使用std::__result_of_memfun_ref
否则为std::__result_of_memfun_deref
。
通过上面两层的判断,已经成功的将不同的调用方法进行了分类,接下来就是使用decltype
来获取返回值了。这一部分就很简单了,由上面的几次分类,分为了五种不同的调用(加上不同的对象)。
__invoke_memfun_ref
(std::declval<_Tp1>().*std::declval<_Fp>())(std::declval<_Args>()...)
__invoke_memfun_deref
((*std::declval<_Tp1>()).*std::declval<_Fp>())(std::declval<_Args>()...)
3. `__invoke_memobj_ref`
```C++
std::declval<_Tp1>().*std::declval<_Fp>()
__invoke_memobj_deref
(*std::declval<_Tp1>()).*std::declval<_Fp>()
__invoke_other
std::declval<_Fn>()(std::declval<_Args>()...)
但是比较好奇的是,程序没有直接通过上面的方法获得,而是构造了一个return_type _S_test(int)
,然后获取的。除了返回的类型外,std::invoke_result
还保存了调用的类型,即之前提到的那五种,这一部分应该是为了实现std::invoke
而保存的。
我的实现
为了学习std::invoke_result
,我也实现了一个类似的ink::invoke_result
。层次结构类似,不过将_S_test()
删去了,改成了直接的实现。
github gist: ink_invoke_result.cpp
后记
阅读标准库里面的实现的确可以学到很多东西,即使以后不会写类似的代码,但是在使用的时候也会对C++有更加清晰的理解。同时也发现了C++目前的一个缺陷,在实现这种接口的时候,免不了要进行多层包装,而每一层的包装,标准库都是直接将其暴露在std
命名空间中,这导致了在使用代码提示的时候极大的降低了提示的效率和美观(一大堆的以下划线开头的item真的非常非常非常难看,而且头大)。
博客原文: https://www.cnblogs.com/ink19/p/cpp_invoke_result.html
std::invoke_result的实现详解的更多相关文章
- std::thread线程库详解(2)
目录 目录 简介 最基本的锁 std::mutex 使用 方法和属性 递归锁 std::recursive_mutex 共享锁 std::shared_mutex (C++17) 带超时的锁 总结 简 ...
- std::thread线程库详解(3)
目录 目录 前言 lock_guard scoped_lock (C++17) unique_lock shared_lock 总结 ref 前言 前两篇的博文分别介绍了标准库里面的线程和锁,这一次的 ...
- std::thread线程库详解(4)
目录 目录 前言 条件变量 一些需要注意的地方 总结 前言 本文主要介绍了多线程中的条件变量,条件变量在多线程同步中用的也比较多.我第一次接触到条件变量的时候是在完成一个多线程队列的时候.条件变量用在 ...
- std::thread线程库详解(5)
目录 目录 前言 信号量 counting_semaphore latch与barrier latch barrier 总结 前言 前面四部分内容已经把目前常用的C++标准库中线程库的一些同步库介绍完 ...
- C++11 std::chrono库详解
所谓的详解只不过是参考www.cplusplus.com的说明整理了一下,因为没发现别人有详细讲解. chrono是一个time library, 源于boost,现在已经是C++标准.话说今年似乎又 ...
- C++11 并发指南六(atomic 类型详解三 std::atomic (续))
C++11 并发指南六( <atomic> 类型详解二 std::atomic ) 介绍了基本的原子类型 std::atomic 的用法,本节我会给大家介绍C++11 标准库中的 std: ...
- C++11 并发指南六( <atomic> 类型详解二 std::atomic )
C++11 并发指南六(atomic 类型详解一 atomic_flag 介绍) 一文介绍了 C++11 中最简单的原子类型 std::atomic_flag,但是 std::atomic_flag ...
- C++11 并发指南五(std::condition_variable 详解)
前面三讲<C++11 并发指南二(std::thread 详解)>,<C++11 并发指南三(std::mutex 详解)>分别介绍了 std::thread,std::mut ...
- C++11 并发指南------std::thread 详解
参考: https://github.com/forhappy/Cplusplus-Concurrency-In-Practice/blob/master/zh/chapter3-Thread/Int ...
随机推荐
- ios获取缓存文件的大小并清除缓存
移动应用在处理网络资源时,一般都会做离线缓存处理,其中以图片缓存最为典型,其中很流行的离线缓存框架为SDWebImage. 但是,离线缓存会占用手机存储空间,所以缓存清理功能基本成为资讯.购物.阅读类 ...
- Promise.all()使用实例
一.什么是Promise.all()? 在说这个之前要先说清楚promise.promise就是一个对象,专门用来处理异步操作的. 而Promise.all方法用于将多个 Promise 实例,包装成 ...
- DOI技术扫盲一
DOI: desktop office intergration 桌面办公软件集成简单的将,就是我们在Windows桌面中打开的办公软件(如:word,excel,pdf等等)可以在SAP系统进 ...
- BAPI_PO_CHANGE
这两天用BAPI更改采购订单,遇到了一些问题,最后调试解决了.记录如下吧.要修改的是采购订单的物料号和批次,在网上看到其它人写过关于 BAPI_PO_CHANGE的用法,但是具体问题还要具体分析啊. ...
- ObjectMapper将josn字符串转化为List
一.利用ObjectMapper将json字符串转为List Student.java package objectmapper; import java.io.Serializable; publi ...
- linux自定义安装位置安装jdk
注:本文系参考网络内容及本人实践得出 1 下载jdk安装包 下载地址:https://www.oracle.com/java/technologies/javase/javase-jdk8-downl ...
- 十九、更改LCD显示屏
一.裸机修改 之前测试用的屏幕是480*272的分辨率,现在要换成800*480的屏,因此要对软件代码进行修改. 对于裸机驱动而言,主要有两个点需要注意,一个是屏幕分辨率变了,因此初始化的时候与屏幕分 ...
- JavaScript中函数的定义!
JavaScript中函数的定义! 1 自定义函数(命名函数) function fun() {}; 2 函数表达式(匿名函数) var fun = function () {}; 3 利用 new ...
- 基于nginx的频率控制方案思考和实践
基于nginx的频率控制方案思考 标签: 频率控制 nginx 背景 nginx其实有自带的limit_req和limit_conn模块,不过它们需要在配置文件中进行配置才能发挥作用,每次有频控策略的 ...
- P6739 [BalticOI 2014 Day1] Three Friends 题解
目录 写在前面 Solution 何为字符串哈希(可跳过): Code 写在前面 P6739 [BalticOI 2014 Day1] Three Friends 听说这题可以用比较暴力的做法过,比如 ...