使用模板包装C风格API进行调用
这可以说是一个非常经典的需求,并且它涉及到的模板知识并不多,主要其实也就是可变参数,元组的处理,更多的还是一个思路写法,经典的 void* + 变参模板。
我们的写法完全参照 MSVC STL 实现的 std::thread。在阅读本节内容之前,希望各位已经学习了现代 C++ 并发编程教程中,std::thread 的构造-源码解析。因为 std::thread 构造函数实际就是将我们传入的所有可调用对象、参数,包装为函数指针,和 void* 参数,调用 win32 的创建线程的函数 _beginthreadex。
简单来说,我们需要写一个类包装一个这样的函数 f ,支持任意可调用对象与任意类型和个数的参数,最终都执行函数 f。
void f(unsigned(*start_address)(void*),void *args){
start_address(args);
std::cout << "f\n";
}它和创建线程的函数很像,一个函数指针是要执行的函数,一个 void* 是参数。
答案与解释
答案如下:
struct X {
template<typename Fn, typename ...Args>
X(Fn&& func, Args&&... args) {
using Tuple = std::tuple<std::decay_t<Fn>, std::decay_t<Args>...>;
auto Decay_copied = std::make_unique<Tuple>(std::forward<Fn>(func), std::forward<Args>(args)...);
auto Invoker_proc = start<Tuple>(std::make_index_sequence<1 + sizeof...(Args)>{});
f(Invoker_proc, Decay_copied.release());
}
template <typename Tuple, std::size_t... Indices>
static constexpr auto start(std::index_sequence<Indices...>) noexcept {
return &Invoke<Tuple, Indices...>;
}
template <class Tuple, std::size_t... Indices>
static unsigned int Invoke(void* RawVals) noexcept {
const std::unique_ptr<Tuple> FnVals(static_cast<Tuple*>(RawVals));
Tuple& Tup = *FnVals.get();
std::invoke(std::move(std::get<Indices>(Tup))...);
return 0;
}
};测试结果。
其实很简单,就三个函数而已。这里的难点只是将我们的可调用对象转换为 unsigned(*start_address)(void*) 这样类型的函数指针以及处理可变参数罢了。我们的做法也很简单,利用模板,做了一个代码生成,实际我们传递的是静态成员函数模板 Invoke 给函数 f 调用,当然,是实例化之后的,还用到了 start 函数。传递的所有参数则使用了一个元组存储副本,由独占的智能指针管理。最终都传递给函数 f 调用。
好,接下来我们来一句一句解析:
using Tuple = std::tuple<std::decay_t<Fn>, std::decay_t<Args>...>;定义了一个元组别名,其元组的模板类型参数就是传递给构造函数的所有对象的类型。auto Decay_copied = std::make_unique<Tuple>(std::forward<Fn>(func), std::forward<Args>(args)...);定义一个独占智能指针,指向了一个元组,其存储了传递给构造函数的所有的参数的副本。auto Invoker_proc = start<Tuple>(std::make_index_sequence<1 + sizeof...(Args)>{});调用静态成员函数模板start得到一个普通函数指针。这里需要详细展开。传递了类型参数Tuple,已经使用std::make_index_sequence制造了一个可变参数序列,用来遍历元组。std::index_sequence和std::make_index_sequence的用法我们用一个简单示例介绍一下。cpptemplate <typename Tuple, std::size_t... Indices> static constexpr auto start(std::index_sequence<Indices...>) noexcept { return &Invoke<Tuple, Indices...>; }将模板参数转发给
Invoke进行实例化,获取这个静态成员函数模板的地址,也就是普通符合类型要求的函数指针了。cpptemplate <class Tuple, std::size_t... Indices> static unsigned int Invoke(void* RawVals) noexcept { const std::unique_ptr<Tuple> FnVals(static_cast<Tuple*>(RawVals)); Tuple& Tup = *FnVals.get(); std::invoke(std::move(std::get<Indices>(Tup))...); return 0; }我们的
Invoke利用模板生成代码,支持了所有可调用类型,以及遍历元组的参数。**无非是把void*指针转换为正确的类型再去使用罢了,而这个“正确的类型”,通过模板传递。**最终的调用,在std::invoke(std::move(std::get<Indices>(Tup))...);这一行,如你所见,默认移动,和std::thread一样。f(Invoker_proc, Decay_copied.release());将函数指针Invoker_proc和存储了传递给构造函数的所有参数的副本的智能指针Decay_copied释放所有权,返回原始指针,用来调用 C 风格函数 f。
使用示例
void func(int& a){
std::cout << &a << '\n';
}
int main(){
int a{};
std::cout << &a << '\n';
X{ func,a };
}测试代码。
和 std::thread 一样,上面代码无法通过编译,”invoke 未找到匹配的重载函数“。原因很简单,我们上面的代码展示了,最终的 invoke 调用是用了 std::move 的,参数被转换为右值表达式,形参类型是左值引用,左值引用不能引用右值表达式自然不行了。
void func(const int& a){
std::cout << &a << '\n';
}
int main(){
int a{};
std::cout << &a << '\n';
X{ func,a };
}测试代码。
我们还可以将 func 的形参类型改为 const int& ,这可以通过编译,因为 const int& 可以引用右值表达式。当然了,打印的地址不同。原因也很简单,我们说了,智能指针存储的是参数副本,元组类型是使用 std::decay_t 删除了 CV 与引用限定的。
void func(const int& a){
std::cout << &a << '\n';
}
int main(){
int a{};
std::cout << &a << '\n';
X{ func,std::ref(a) };
}测试代码。
同样的,和 std::thread 一样使用 std::ref 即可解决。
总结
如果感受到难度,重新细读std::thread 的构造-源码解析 ,或者再把第一部分-基础初始知识好好学习。