背景
半个月前开始接触了Nginx模块开发,现在也写了两个Hello World那种性质的小模块了。由于Nginx采用C语言编写,在Nginx模块开发的过程中最令我难受的是需要向Nginx传递若干个函数指针类型的“钩子”(hook),C语言的函数指针就是ReturnType (*)(Parameter... args)
。当然C语言是不会有C++11的可变参数模板的,我只是想强调一下在本文中所说的函数指针就是这样一个简陋的东西。
这样的函数指针让我无数次想要给开发Nginx的俄国人送去亲切的问候,Nginx的0.1.0是在21世纪发布的,那时候已经有了C++98标准,C++98里已经有了ReturnType operator()(Parameter... args)
(函数调用运算符),我也不知道为什么他们决定用C语言开发Nginx。
考虑到C++11有了各种各样的可调用对象,无论是实现了operator()(...)
的对象,或是std::function
,或是Lambda。相对于修改Nginx源代码而言,还是考虑着如何将可调用对象转换成C语言的函数指针相对更合适一些。于是就开始了百度…………
百度里的大多数帖子都说的是不行,不可能把函数对象转换成函数指针。除了去年的一位作者遇到了和我类似的一个问题,发了一个帖子记录了他的实现。
C++ 中把 lambda 优雅地转化为函数指针
虽然因为没有注册该网站的会员,有些图片看不清楚(注册居然需要10元人民币,不愿意花,再见),但是还是从中看清楚了几张截图。就着这几张截图的思路,我一步一步地找到了不完美的解决方案。
在这一节的最后强调一下,我的目标是将实现了int operator()(int a)
函数的可调用对象,可以通过int (*)(int)
类型的函数指针去调用。在这里输入输出的int只是为了方便文字说明。
解决过程
第一版:从帖子里抄代码
从帖子里抄到的源代码如下
template<class Lambda>
struct CStyleCallbackFetcher_v1
{
template<typename R, typename ... Args>
static R callback(void* vlambda, Args... args)
{
//reference:http://www.vcchar.com/thread-20657-1-1.html
//这里的(Lambda *)是旧式的类型转换,将vlambda转换成(Lambda*)的类型
//return (*(Lambda*)vlambda)(std::forward<Args>(args)...);
return (*reinterpret_cast<Lambda*>(vlambda))(std::forward<Args>(args)...);
//这一句的意思是首先解引用Lamda *的指针得到Lamda,再调用Lambda得到返回值R
//callback函数模板会针对不同的函数参数和返回值类型实例化不同的函数
//这个callback函数再接收不同的Lambda对象,运行Lambda得到不同的返回值
//但是,这不是我想要的东西
}
template<typename R, typename ... Args>
using callback_t = R(*)(void *vlambda, Args ... args);
template<typename R, typename ... Args>
operator callback_t<R, Args...>() const
{
return &callback<R, Args...>;
}
};
template<class Lambda>
CStyleCallbackFetcher_v1<Lambda> getCallbackFetcher_v1(Lambda &)
{
return CStyleCallbackFetcher_v1<Lambda>();
}
这段代码和原帖子里的基本一致,我仅就自己的理解加了注释,并把一个C-style的指针类型转换修改成reinterpret_cast。
使用方法是
auto add = [](int a, int b)->int {return (a + b); };
int(*fp)(void *, int, int) = getCallbackFetcher_v1(add);
int c = fp(&add, 1, 2);
想了一下,原作者这个思路的核心是用类模板,如果参数类型和返回类型组成的Tuple不同,就实例化成不同的类。但是觉得他的这样一个设计略微不是那么干净。比如这个类不必要是模板类,完全可以只用函数模板传参。并且没必要在类外单独实现一个函数。并且他的这样一个方案并不是我所想要的,因为他可以在函数指针中传递一个void *指向它的上下文,类似于this指针的用途。但是我不可以。
基于此,我修改出了一个版本。
第二版:精简原设计
struct CStyleCallbackFetcher_v2
{
template<class Callable, typename R, typename ... Args >
static R callback(void* vlambda, Args... args)
{
//reference:http://www.vcchar.com/thread-20657-1-1.html
//这里的(Lambda *)是旧式的类型转换,将vlambda转换成(Lambda*)的类型
//return (*(Lambda*)vlambda)(std::forward<Args>(args)...);
return (*reinterpret_cast<Callable*>(vlambda))(std::forward<Args>(args)...);
//这一句的意思是首先解引用Lamda *的指针得到Lamda,再调用Lambda得到返回值R
//callback函数模板会针对不同的函数参数和返回值类型实例化不同的函数
//这个callback函数再接收不同的Lambda对象,运行Lambda得到不同的返回值
//但是,这不是我想要的东西
}
template<typename R, typename ... Args>
using c_style_function_pointer = R(*)(void *vlambda, Args ... args);
//因为核心是获得callback函数的地址&callback,而且我们现在的类不是模板类了,
//所以v1那个辅助函数不需要
//我们把函数调用运算符“()”改成了一个静态的辅助函数getPointer()用来获得编译器实例化的callback函数的地址
template<class Callable,typename R, typename ... Args>
static c_style_function_pointer<R, Args...> getPointer()
{
return &callback<Callable, R, Args...>;
}
//这个重载的版本是因为可调用对象可能是Lambda,Lambda是匿名的类,
//获得类型比较麻烦,而且decltype()比较“肮脏”
//但是写到这里发现根据C++规范还是要在模板参数里写typename Callable的
template<class Callable, typename R, typename ... Args>
static c_style_function_pointer<R, Args...> getPointer(Callable &callable)
{
return &callback<Callable, R, Args...>;
}
};
这个类没有做出太大的改变,仅仅是根据自己的喜好和理解将Lambda
类型参数移动到了函数模板的类型参数列表之中,放在返回值的前面。所以第一版中,类外用于根据Lambda
类型实例化类模板的辅助函数不再需要。删去。
我实现了以下两个重载的函数,可能看起来有些奇怪。
template<class Callable,typename R, typename ... Args>
static c_style_function_pointer<R, Args...> getPointer();
template<class Callable, typename R, typename ... Args>
static c_style_function_pointer<R, Args...> getPointer(Callable &callable);
无参数的版本是先写的,然后想到因为传入的可调用对象可能是Lambda,Lambda是匿名的类,获取类型有点麻烦,我总是觉得decltype()
有点看着不舒服,所以重载了一个有参数的版本,想用传一个可调用对象作为实参,让编译器自行推导类型。但是写完发现不对啊,因为我要写返回值R
和形参Args...
,并且现在我们的类不是模板类了,根本不可能不指定Callable类型嘛。
但是还是留着了,因为C++函数重载没有冲突。并且下文还会用到。
失败的想法
在第二版已经比较简洁明了,但是第二版的实现是建立在可以向函数传递一个可调用对象的指针的前提下的。但是我们的目标是只能向获得的函数指针传递和可调用对象参数列表相同的参数。
很自然地,我们首先可以想到可以向模板函数的参数列表多加一个可调用对象的地址,然后在callback
函数中解引用得到对象。类似下面这样。
template<class Callable,uintptr_t callableAddr, typename R, typename ... Args >
static R callback(Args... args)
{
Callable *a = &callableAddr;
}
这样在数学上而言,在柯里化的意义上完全没有问题,我们可以通过(可调用对象,参数列表,返回类型)这样一个Tuple,完全确定所要调用的方法(method)。
但是,在C++语法中有问题,一个对象的地址是运行期的量,而模板实例化是在编译期完成的。模板非类型参数必须是运行期常量。
于是就陷入了黑暗之中…………
第三版:在类模板中的静态成员存储Callable指针
根据《C++ Primer》,在不同的独立编译的源文件中,相同模板参数所实例化的类是不同的。考虑到我们是在开发Nginx模块,本文所述的组件是为了与Nginx核心部分交互,我们绝大多数情况下在一个源文件中只会使用相同模板参数的实例化对象一次。所以,我们也许可以不完美地仅用可调用对象的类型作为类模板参数,实例化出不同的类,并在类的静态区中存储Callable*
。
template<typename Callable>
struct CStyleCallbackFetcher_v3
{
template<typename R, typename ... Args >
static R callback(Args... args)
{
//reference:http://www.vcchar.com/thread-20657-1-1.html
//这里的(Lambda *)是旧式的类型转换,将vlambda转换成(Lambda*)的类型
//return (*(Lambda*)vlambda)(std::forward<Args>(args)...);
return (*reinterpret_cast<Callable*>(callableObject))(std::forward<Args>(args)...);
//这一句的意思是首先解引用Lamda *的指针得到Lamda,再调用Lambda得到返回值R
//模板会针对不同的函数签名(typename)展开成不同的callback函数
//这个callback函数再接收不同的Lambda对象,运行Lambda得到不同的返回值
//但是,这不是我想要的东西
//但是我想Nginx那个应该没有必要抽象到这种程度
//因为Nginx根本就没有提供上下文注入自定义的对象
}
template<typename R, typename ... Args>
using c_style_function_pointer = R(*)(Args ... args);
//因为核心是获得callback函数的地址&callback,而且我们现在的类不是模板类了,
//所以v1那个辅助函数不需要
//而且我们现在的类不是模板类了,那么也就不能重载函数调用运算符“()”了
//所以现在我们加入了一个静态的辅助函数用来获得编译器实例化的callback函数的地址()
//v3:记得上文我们那个失败的尝试吗,在这里就有用了
template<typename R, typename ... Args>
static c_style_function_pointer<R, Args...> getPointer()
{
return &callback<R, Args...>;
}
static Callable *callableObject;
//这个重载的版本是因为可调用对象可能是Lambda,Lambda是匿名的类,
//获得类型比较麻烦,而且decltype()比较“肮脏”
//但是写到这里发现根据C++规范还是要在模板参数里写typename Callable的
template<typename R, typename ... Args>
static c_style_function_pointer<R, Args...> getPointer(Callable &callable)
{
callableObject = &callable;
return &callback<R, Args...>;
}
};
template<typename Callable>
Callable *CStyleCallbackFetcher_v3<Callable>::callableObject = NULL;
第三版的修改无需多言。我们在第二版中多余的getPointer(Callable &)
在这里就发挥作用了。代码逻辑很清晰,我甚至懒得解释。
但是第三版仍然面对着一些隐患。假设我们在同一个CPP源文件的前半部分已经实例化过CStyleCallbackFetcher_v3<int>
并且设置了静态成员Callable*
并拿到了对应的C-style函数指针传递给了Nginx。但是在Nginx调用之前,在那个CPP源文件的其他部分“不期待地”修改了其中的Callable*
……………………
所以,我们还是有必要用一些签名,加强这个类模板实例的唯一性
第四版:洗澡的时候想到的宏LINE
我洗澡比较慢,时间比较长。但是以往我有一些点子就是在洗澡的时候想到的,这次也是。
基于上面所说的隐患,我们有必要给这个类增加签名。洗澡的时候想到了C++预处理器提供了一个宏LINE,表示该宏所在源文件的行号,主要用于调试,我们可以将它作为类成员模板参数。这样,像下面这两行所实例化的类是不同的,所以输出的Callable*
指针的地址自然也是不同的。
std::cout << &CStyleCallbackFetcher_v4<decltype(add), __LINE__>::callableObject << std::endl;
std::cout << &CStyleCallbackFetcher_v4<decltype(add), __LINE__>::callableObject << std::endl;
第四版的代码如下,没什么好解释的。
template<typename Callable,uintptr_t uniqueSignature>
struct CStyleCallbackFetcher_v4
{
template<typename R, typename ... Args >
static R callback(Args... args)
{
//reference:http://www.vcchar.com/thread-20657-1-1.html
//这里的(Lambda *)是旧式的类型转换,将vlambda转换成(Lambda*)的类型
//return (*(Lambda*)vlambda)(std::forward<Args>(args)...);
return (*reinterpret_cast<Callable*>(callableObject))(std::forward<Args>(args)...);
//这一句的意思是首先解引用Lamda *的指针得到Lamda,再调用Lambda得到返回值R
//模板会针对不同的函数签名(typename)展开成不同的callback函数
//这个callback函数再接收不同的Lambda对象,运行Lambda得到不同的返回值
//但是,这不是我想要的东西
//但是我想Nginx那个应该没有必要抽象到这种程度
//因为Nginx根本就没有提供上下文注入自定义的对象
}
template<typename R, typename ... Args>
using c_style_function_pointer = R(*)(Args ... args);
//因为核心是获得callback函数的地址&callback,而且我们现在的类不是模板类了,
//所以v1那个辅助函数不需要
//而且我们现在的类不是模板类了,那么也就不能重载函数调用运算符“()”了
//所以现在我们加入了一个静态的辅助函数用来获得编译器实例化的callback函数的地址()
static void testFunc()
{
std::cout << uniqueSignature << std::endl;
}
//v3:记得上文我们那个失败的尝试吗,在这里就有用了
template<typename R, typename ... Args>
static c_style_function_pointer<R, Args...> getPointer()
{
return &callback<R, Args...>;
}
static Callable *callableObject;
//这个重载的版本是因为可调用对象可能是Lambda,Lambda是匿名的类,
//获得类型比较麻烦,而且decltype()比较“肮脏”
//但是写到这里发现根据C++规范还是要在模板参数里写typename Callable的
template<typename R, typename ... Args>
static c_style_function_pointer<R, Args...> getPointer(Callable &callable)
{
callableObject = &callable;
return &callback<R, Args...>;
}
};
template<typename Callable,uintptr_t signature>
Callable *CStyleCallbackFetcher_v4<Callable,signature>::callableObject = NULL;
这样,除非拥有代码修改权限的人做出一些骚操作,否则这个类模板的每一个实例都只会用一次,保证静态成员不会被修改。
但是,第四版的代码在Visual Studio 2015编译失败,原因是编译器认为LINE不是编译期常量。但是这段代码在GCC 7下没有问题。考虑到Nginx的目标平台以Linux为主,且可以手动设置编译器常量作为签名。这个问题暂不解决。
Brisk Core
2019.6.11-2019.6.12