C++ function_traits 原理及应用介绍
一、背景
最近在看一个rpc 的源码遇到了一些既陌生又熟悉的代码,如下:
#ifndef REST_RPC_META_UTIL_HPP
#define REST_RPC_META_UTIL_HPP
#include "cplusplus_14.h"
#include <functional>
namespace rest_rpc {
template <typename... Args, typename Func, std::size_t... Idx>
void for_each(const std::tuple<Args...> &t, Func &&f,
nonstd::index_sequence<Idx...>) {
(void)std::initializer_list<int>{(f(std::get<Idx>(t)), void(), 0)...};
}
template <typename... Args, typename Func, std::size_t... Idx>
void for_each_i(const std::tuple<Args...> &t, Func &&f,
nonstd::index_sequence<Idx...>) {
(void)std::initializer_list<int>{
(f(std::get<Idx>(t), std::integral_constant<size_t, Idx>{}), void(),
0)...};
}
template <typename T> struct function_traits;
template <typename Ret, typename Arg, typename... Args>
struct function_traits<Ret(Arg, Args...)> {
public:
enum { arity = sizeof...(Args) + 1 };
typedef Ret function_type(Arg, Args...);
typedef Ret return_type;
using stl_function_type = std::function<function_type>;
typedef Ret (*pointer)(Arg, Args...);
typedef std::tuple<Arg, Args...> tuple_type;
typedef std::tuple<
nonstd::remove_const_t<nonstd::remove_reference_t<Args>>...>
bare_tuple_type;
using args_tuple =
std::tuple<std::string, Arg,
nonstd::remove_const_t<nonstd::remove_reference_t<Args>>...>;
using args_tuple_2nd =
std::tuple<std::string,
nonstd::remove_const_t<nonstd::remove_reference_t<Args>>...>;
};
//普通函数
template <typename Ret> struct function_traits<Ret()> {
public:
enum { arity = 0 };
typedef Ret function_type();
typedef Ret return_type;
using stl_function_type = std::function<function_type>;
typedef Ret (*pointer)();
typedef std::tuple<> tuple_type;
typedef std::tuple<> bare_tuple_type;
using args_tuple = std::tuple<std::string>;
using args_tuple_2nd = std::tuple<std::string>;
};
//函数指针
template <typename Ret, typename... Args>
struct function_traits<Ret (*)(Args...)> : function_traits<Ret(Args...)> {};
//std::function
template <typename Ret, typename... Args>
struct function_traits<std::function<Ret(Args...)>>
: function_traits<Ret(Args...)> {};
//member function
template <typename ReturnType, typename ClassType, typename... Args>
struct function_traits<ReturnType (ClassType::*)(Args...)>
: function_traits<ReturnType(Args...)> {};
template <typename ReturnType, typename ClassType, typename... Args>
struct function_traits<ReturnType (ClassType::*)(Args...) const>
: function_traits<ReturnType(Args...)> {};
// 特化用于 Lambda 表达式和函数对象
template <typename Callable>
struct function_traits : function_traits<decltype(&Callable::operator())> {
};
template <typename T>
using remove_const_reference_t =
nonstd::remove_const_t<nonstd::remove_reference_t<T>>;
template <size_t... Is>
auto make_tuple_from_sequence(nonstd::index_sequence<Is...>)
-> decltype(std::make_tuple(Is...)) {
std::make_tuple(Is...);
}
template <size_t N>
constexpr auto make_tuple_from_sequence()
-> decltype(make_tuple_from_sequence(nonstd::make_index_sequence<N>{})) {
return make_tuple_from_sequence(nonstd::make_index_sequence<N>{});
}
namespace detail {
template <class Tuple, class F, std::size_t... Is>
void tuple_switch(const std::size_t i, Tuple &&t, F &&f,
nonstd::index_sequence<Is...>) {
(void)std::initializer_list<int>{
(i == Is &&
((void)std::forward<F>(f)(std::integral_constant<size_t, Is>{}), 0))...};
}
} // namespace detail
template <class Tuple, class F>
inline void tuple_switch(const std::size_t i, Tuple &&t, F &&f) {
constexpr auto N = std::tuple_size<nonstd::remove_reference_t<Tuple>>::value;
detail::tuple_switch(i, std::forward<Tuple>(t), std::forward<F>(f),
nonstd::make_index_sequence<N>{});
}
template <int N, typename... Args>
using nth_type_of = nonstd::tuple_element_t<N, std::tuple<Args...>>;
template <typename... Args>
using last_type_of = nth_type_of<sizeof...(Args) - 1, Args...>;
} // namespace rest_rpc
#endif // REST_RPC_META_UTIL_HPP
之前了解过type_traits 及萃取的一些知识,但在项目中使用的很少。阅读这个rcp库是因为最近项目在往分布式方向做规划,坦白讲这块的经验很少,对分布式架构知之甚少。于是提前了解下当前分布式的技术架构免得到时开发时手忙脚乱。我相信机会是留给有准备的人,未雨绸缪并非坏事。一线牛马且是一个对技术有追求的牛马每年高低得读几个开源库。等我把这块业务搞上线之后也准备搞一个开源的rcp 库出来。
读到这个库上述代码时,发现一个简单的函数调用居然整了那么多模板,而且有好多地方作凭我当前的功力阅读起来非常吃力,危机感油然而生。顿时感觉自己好渺小,我这条小船随时有可能在C++ 这个知识大海里沉没。于是抱着敬畏之心整理了这个知识点。整理的不到位的地方欢迎批评指正。
本文将详细介绍C++ function_traits的概念、实现原理、应用场景。通过深入的代码示例和详细解析来了解function_traits的实际应用和未来发展方向。
二、概念
2.1 定义与用途
function_traits 是一种模板结构,用于在编译期提取函数的类型信息,包括返回类型、参数类型及参数个数。这在编写泛型代码和高级编程时非常有用,尤其是在需要对函数类型进行操作或分析的场景中。function_traits 通过模板元编程技术,在编译期解析出函数的详细类型信息,从而使程序员能够在编写泛型代码时获得更高的灵活性和类型安全性。
2.2 基本结构
function_traits 的实现涉及模板特化技术,通过特化模板,function_traits 能够处理不同类型的函数,包括普通函数、函数指针和成员函数。
假设现有一个需求:
写一个通用的函数打印函数、成员函数、静态函数、lambda的返回类型、参数个数、参数类型。
有思路没?没有吧,拿到这个需求时我也没有任何思路,无从下手,感觉触及到了自己的知识盲区。但没关系,遇到问题不逃避,不畏惧,敢于直面自己的短板并补齐它才是一个合格的牛马。下面有请本文主角function_traits 登场:
//定义基本模板
template <typename T>
struct function_traits;
//特化用于普通函数类型
template <typename R, typename... Args>
struct function_traits<R(Args...)> {
using result_type = R;
static const std::size_t arity = sizeof...(Args);
template <std::size_t N>
struct arg {
static_assert(N < arity, "参数索引超出范围");
using type = typename std::tuple_element<N, std::tuple<Args...>>::type;
};
using tuple_type = std::tuple<std::remove_cv_t<std::remove_reference_t<Args>>...>;
using bare_tuple_type = std::tuple<std::remove_const_t<std::remove_reference_t<Args>>...>;
};
上述代码用到了以下几处知识点:
- 模板:因为需求中涉及到函数,静态函数,成员函数,lambda 表达式所以选用这玩意
- 不定参数... :因为要打印的目标函数参数不定
- 函数萃取
为了更好的理解,我将逐行解释上述这段代码:
//定义基本模板 这个不用过多解释了吧
template <typename T>
struct function_traits;
//对上述模板进行特化 用于普通函数类型
//其中R 代表函数的返回值
//typename... Args 代表函数的参数 ,可变的
template <typename R, typename... Args>
struct function_traits<R(Args...)> {
using result_type = R; //返回类型
static const std::size_t arity = sizeof...(Args); //参数 个数
template <std::size_t N>
struct arg {
static_assert(N < arity, "参数索引超出范围");
using type = typename std::tuple_element<N, std::tuple<Args...>>::type;
};
//创建一个元组(tuple),包含所有参数类型(去除引用和cv限定符)
using tuple_type = std::tuple<std::remove_cv_t<std::remove_reference_t<Args>>...>;
//类似于 tuple_type,但仅去除 const 限定符,保留 volatile 限定符(如果存在)
using bare_tuple_type = std::tuple<std::remove_const_t<std::remove_reference_t<Args>>...>;
};
使用的代码如下:
void add(int a, std::string b, float c){
return ;
}
void printFunInfo() {
using MyFunctionTraits = function_traits<decltype(add)>;
// 返回类型
std::cout << "219-----"<<typeid(MyFunctionTraits::result_type).name() << std::endl; // 输出 void
// 参数数量
std::cout << "参数数量 = "<<MyFunctionTraits::arity << std::endl; //
// 第一个参数类型
using FirstParamType = typename MyFunctionTraits::template arg<0>::type;
std::cout << "第一个参数类型 "<<typeid(FirstParamType).name() << std::endl; // 输出 int
// 元组类型
std::cout << "元组类型 = "<<typeid(MyFunctionTraits::tuple_type).name() << std::endl;
// 输出 std::tuple<int, class std::basic_string<char, struct std::char_traits<char>, class std::allocator<char>>, float>
// 去除const限定符的元组类型
std::cout << "去除const限定符的元组类型 = " << typeid(MyFunctionTraits::bare_tuple_type).name() << std::endl;
}
int main() {
printFunInfo();
return 0;
}
输出的结果为:
上述代码仅实现了获取普通函数的信息,对于指针,成员函数,lambda 表达式还需要再写几个特化模板:
2.2.2 函数指针
// 函数指针
template<typename ReturnType, typename... Args>
struct function_traits<ReturnType(*)(Args...)> : function_traits<ReturnType(Args...)> {};
当模板参数是一个函数指针类型(如 ReturnType(*)(Args...))时,function_traits 的行为。它继承自普通函数版本的 function_traits<ReturnType(Args...)>,这意味着函数指针的所有特性(如返回类型、参数数量和类型等)都按照普通函数的方式来提取。
2.2.3 std::function
// std::function
template<typename ReturnType, typename... Args>
struct function_traits<std::function<ReturnType(Args...)>> : function_traits<ReturnType(Args...)> {};
尽管 std::function 可以存储各种可调用对象(函数、函数指针、lambda 表达式、成员函数指针等),但在类型层面,它仍然被视为具有特定签名的实体。因此,这里的 function_traits 特化同样继承自普通函数版本,以提取封装在其内的函数特性。
2.2.4 成员函数
// 成员函数
#define FUNCTION_TRAITS(...)\
template <typename ReturnType, typename ClassType, typename... Args>\
struct function_traits<ReturnType(ClassType::*)(Args...) __VA_ARGS__> : function_traits<ReturnType(Args...)>{};\
FUNCTION_TRAITS()
FUNCTION_TRAITS(const)
FUNCTION_TRAITS(volatile)
FUNCTION_TRAITS(const volatile)
利用宏定义处理成员函数的特性。这里的 VA_ARGS 是可变参数占位符,允许接收成员函数的任意 cv 限定符和 ref-qualifier(如 const、volatile、&、&& 等)。该特化表明成员函数的 function_traits 继承自相应的非成员函数版本,忽略了类类型 ClassType,重点关注函数的返回类型和参数列表。
通过 VA_ARGS 来捕获任何额外的cv限定符和其他可能的ref-qualifier。例如,当使用宏 FUNCTION_TRAITS(const) 时,实际上会生成这样的结构:
template <typename ReturnType, typename ClassType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const> : function_traits<ReturnType(Args...)> {};
同样地,FUNCTION_TRAITS(volatile) 和 FUNCTION_TRAITS(const volatile) 分别生成:
template <typename ReturnType, typename ClassType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) volatile> : function_traits<ReturnType(Args...)> {};
template <typename ReturnType, typename ClassType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const volatile> : function_traits<ReturnType(Args...)> {};
这样就可以处理成员函数的各种cv限定符组合,而无需分别为每种情况显式编写特化版本。在处理成员函数时,function_traits 关注的是函数本身的签名(返回类型和参数类型),而非类类型或cv限定符,因此这里都继承自普通函数签名的 function_traits 版本。
使用例子:
/**
* 调用成员函数
* @tparam F 成员函数
* @tparam Self 实例对象
* @tparam Indexes 参数的索引
* @tparam Args 参数列表
* @param f 成员函数地址
* @param self 实例对象
* @param tup 成员函数参数元组
*/
template <typename F, typename Self, size_t... Indexes, typename... Args>
static void call_member_helper(const F &f, Self *self,
std::index_sequence<Indexes...>,
std::tuple<Args...> tup) {
std::cout<< "Indexes 0 = " << std::get<0>(tup) << " 1= " << std::get<1>(tup) << std::endl;
(*self->*f)(std::move(std::get<Indexes>(tup))...);
}
class MemFunc {
public:
MemFunc() {
}
float mem_divide(int a,float b,std::string c) {
printf("测试成员函数 this is 3 args a = %d b= %f c= %s\n",a,b,c.c_str());
return 2.0f;
}
void mem_fun(const int a,int b) {
printf("测试成员函数-----a = %d b = %d \n",a,b);
}
static void static_fun(const int a,int b) {
printf("测试成员静态函数-----a = %d b = %d \n",a,b);
}
};
int main() {
std::cout << "测试通用函数调用-------------:" <<std::endl;
std::cout << "测试普通函数 Add: " << invoke_function(add, 3, 4) << std::endl; // 输出: Add: 7
std::cout << "测试普通函数 Concatenate: " << invoke_function(concatenate, "Hello, ", "World!") << std::endl; // 输出: Concatenate: Hello, World!
std::cout << "测试普通静态函数 plus: " << invoke_function(plus, 2, 4) << std::endl; // 输出: Concatenate: Hello, World!
//函数指针
int (*addPrt)(int,int) = add;
std::cout << "测试函数指针 : " << invoke_function(addPrt, 30, 4) << std::endl; // 输出: Add: 7
// 使用 Lambda 表达式
auto lambda = [](int x, int y) -> int {
return x * y;
};
std::cout << "测试 Lambda: " << invoke_function(lambda, 5, 6) << std::endl;
printf("使用function_traits 封装通用的函数调用器--------\n");
MemFunc* memFunc = new MemFunc();
std::tuple<int,float,std::string> argsTuple = std::make_tuple(2,3.0f,"123");
call_member_helper(&MemFunc::mem_divide,&memFunc,
std::make_index_sequence<3>{},
argsTuple);
std::tuple<int,int> argsTuple2 = std::make_tuple(1,10);
call_member_helper(&MemFunc::mem_fun,&memFunc,
std::make_index_sequence<2>{},
argsTuple2);
printf("232--------测试成员函数----------\n");
std::tuple<int,int> argsTuple3 = std::make_tuple(10,100);
call_static_member_helper(&MemFunc::static_fun,&memFunc,
std::make_index_sequence<2>{},
argsTuple2);
MemFunc* memFunc = new MemFunc();
std::tuple<int,float,std::string> argsTuple = std::make_tuple(2,3.0f,"123");
call_member_helper(&MemFunc::mem_divide,&memFunc,
std::make_index_sequence<3>{},
argsTuple);
std::tuple<int,int> argsTuple2 = std::make_tuple(1,10);
call_member_helper(&MemFunc::mem_fun,&memFunc,
std::make_index_sequence<2>{},
argsTuple2);
printf("232--------测试成员函数----------\n");
std::tuple<int,int> argsTuple3 = std::make_tuple(10,100);
call_static_member_helper(&MemFunc::static_fun,&memFunc,
std::make_index_sequence<2>{},
argsTuple2);
}
测试结果:
2.2.5 函数对象
// 函数对象
template<typename Callable>
struct function_traits : function_traits<decltype(&Callable::operator())> {};
模板 function_traits 的这个实现是用来获取任意可调用对象(包括函数、函数指针、lambda 表达式、重载了 operator() 的类实例等)的元信息。对于函数对象,我们通常关注其 operator() 的签名来推断其行为特征。
这里的模板定义首先尝试获取 Callable 类型上的 operator() 成员函数的地址,并以其类型作为 decltype(&Callable::operator()) 进行进一步处理。这种做法利用了模板递归的特性,因为对于一个函数对象,它的 operator() 就像一个普通的函数一样,具有返回类型和参数列表。
通过这种方式,我们可以提取出 Callable 类型的 operator() 的相关信息,比如返回类型、参数类型等,进而封装到 function_traits 结构体中。这个设计通常会有一个基础特化版本来处理不同类型的可调用对象,特别是非成员函数和特定形式的成员函数指针,以便于提取出它们的特征。
2.2.6 转换
template<typename Function>
typename function_traits<Function>::stl_function_type to_function(const Function& lambda)
{
return static_cast<typename function_traits<Function>::stl_function_type>(lambda);
}
template<typename Function>
typename function_traits<Function>::stl_function_type to_function(Function&& lambda)
{
return static_cast<typename function_traits<Function>::stl_function_type>(std::forward<Function>(lambda));
}
template<typename Function>
typename function_traits<Function>::pointer to_function_pointer(const Function& lambda)
{
return static_cast<typename function_traits<Function>::pointer>(lambda);
}
这段代码提供了两个模板函数 to_function 和一个模板函数 to_function_pointer,它们分别用于将不同的可调用对象(如 Lambda 表达式或其他函数对象)转换为对应的 std::function 类型和函数指针类型。
- to_function 函数模板:
- 接受一个 const 引用或右值引用 Function 类型的参数 lambda。
- 利用 function_traits 模板类获取 Function 类型的 stl_function_type,这是一个与 Function 兼容的 std::function 类型。
- 使用 static_cast 将 lambda 转换为 stl_function_type 类型并返回。
- to_function_pointer 函数模板:
- 接受一个 const 引用的 Function 类型参数 lambda。
- 利用 function_traits 模板类获取 Function 类型的 pointer 类型,这是一个指向与 Function 符合相同签名的函数指针类型。
- 使用 static_cast 将 lambda 转换为 pointer 类型并返回。
需要注意的是,不是所有的可调用对象都能安全地转换为函数指针或 std::function。例如,Lambda 表达式(尤其是那些捕获了环境变量的 Lambda)无法直接转换为函数指针。另外,std::function 会带来一些运行时开销,不适用于对性能要求较高的场景。因此,在实际使用中,应确保转换操作合法且符合预期。
三、应用场景
function_traits 应用场景非常广泛,特别是在底层库的设计中通常能够以较短的代码实现更为复杂的功能。比如rcp 库的设计中,Server 端在使用函数绑定对应的回调函数时就能发挥不错的威力。在一些著名库设计当中经常能看到它的身影。比如rcp库、Boost库。最近正在写一个rcp 库,大量运用了function_traits、std:make_index_sequence 、std:tuple 、std::move、std::forward。等完工之后再将它开源出来,敬请期待!
四、参考文档
https://blog.csdn.net/qq_60755751/article/details/138651148
https://blog.csdn.net/iShare_Carlos/article/details/140808760
https://blog.csdn.net/feng__shuai/article/details/134956822
https://blog.csdn.net/CSDN1292248830/article/details/141392996
https://juejin.cn/post/7028022597916819463