绝大多数OS的thread入口函数大致是这样的:
void_or_error_code entry_point(void *arbitrary_data);
而std::thread的构造函数长这样:
template< class Function, class... Args > explicit thread( Function&& f, Args&&... args );
为了填平两者之间的差异,我们要做的就是把f和args统统打包在一起做成一个void *,然后用一个预定义的plain C function作为entry point,接收这个void *,解开其中的f和args,然后调用。
和某些回答的说法不同,OS thread API不能直接触碰function template,因为它们大都长这样(略去不相关的参数):
error_code create_thread((void_or_error_code(*entry)(void *), void *data);
不用折腾了,你想破头也没办法让这个API直接调用一个function template或者一个lambda,你只能给它提供一个plain function pointer。用std::function<void_or_error()>绕圈子是个办法,但到最后你还是需要把f和args打包在一起,然后再用一个包装函数拆包调用f(args...),然后再把这个函数包装在std::function里,我觉得反倒更麻烦了。
下面一步一步细说:
1、打包f和args成一个void *
当然我们不能真得把f和args直接变成一个void *,至少尺寸肯定不合适,所以我们需要一个数据结构来保存f和arg。另外,我们说过thread entry point必需是一个plain function,所以template戏法对于entry point来说是不能用的,为了让我们的entry point可以使用这些数据,我们需要一个concrete type而不是template。
不得不承认virtual function有时候还是有用的。让我们定义一个基类:
struct thread_data_base { virtual ~thread_data_base(){} virtual void run()=0; };
这个基类给我们提供了一个统一的入口,可以调用实际用户提供的f和args。
2、接下来我们定义一个template,以适配不同类型的f和args:
template<typename F, class... ArgTypes> class thread_data : public thread_data_base { public: thread_data(F&& f_, ArgTypes&&... args_) : fp(std::forward<F>(f_), std::forward<ArgTypes>(args_)...) {} template <std::size_t... Indices> void run2(tuple_indices<Indices...>) { invoke(std::move(std::get<0>(fp)), std::move(std::get<Indices>(fp))...); } void run() { typedef typename make_tuple_indices<std::tuple_size<std::tuple<F, ArgTypes...> >::value, 1>::type index_type; run2(index_type()); } private: /// Non-copyable thread_data(const thread_data&)=delete; void operator=(const thread_data&)=delete; std::tuple<typename std::decay<F>::type, typename std::decay<ArgTypes>::type...> fp; };
在这个template里有一个data member,它是一个tuple,用于保存f和args,这样我们就可以通过将void *data cast成thread_data_base *,然后调用其中的虚函数run来实际调用f(args...),里面的invoke和tuple_indices等下再说。
3、然后我们就可以用一个简单的函数把任意的f和args包装成一个thread_data_base *:
template<typename F, class... ArgTypes> inline thread_data_base *make_thread_data(F&& f, ArgTypes&&... args) { return new thread_data<typename std::remove_reference<F>::type, ArgTypes...>(std::forward<F>(f), std::forward<ArgTypes>(args)...); }
4、thread entry point变得很简单,这样就行了:
void_or_error_code thread_entry(void *data) { std::unique_ptr<thread_data_base> p((thread_data_base *)data); p->run(); // return result of p->run() if error code is required }
异常处理等细节略去。
注意:下节含有大量C++黑魔法,可能会引起阅读者不适,请谨慎前行
5、下面看看invoke,或者说如何通过一个f和args组成的tuple调用f(args...)
简单说,对于一个tuple<F, T1, T2, T3> tp(f, a1, a2, a3),如果你想调用f(a1, a2, a3),你需要:
tp.get<0>()(tp.get<1>(), tp.get<2>(), tp.get<3>());
forward和decay神马的略去不提。
注意这里的1、2、3必须是编译期常数,否则你是没法拿来当template参数的,也就是说,为了调用f(args...),我们必需生成一个编译期的数列,这各编译期的数列就是前面提到的tuple_indices。
有了这个数列,假如它叫Indices,我们就可以这样调用:
tp.get<0>()(tp.get<Indices>()...);
make_tuple_indices就是用来生成Indices的。
为了生成数列[Sp, Ep),我们要做的就是从Sp开始,递归的在已有数列后面加一项,直到满足条件(Sp==Ep)。
让我们先定义tuple_indices:
template <std::size_t...> struct tuple_indices {};
这个类不需要任何成员,所有需要的信息,也就是整个数列,都是template参数,是类型的一部分。
为了让代码清楚一点,我们再绕个圈子:
template <std::size_t Sp, class IntTuple, std::size_t Ep> struct make_indices_imp;
之所以要绕这个圈子,是因为我们的make_tuple_indices应该长这样:
template <std::size_t Ep, std::size_t Sp> struct make_tuple_indices {...};
但在生成这个数列的过程中,为了方便,我们想把数列当前项直接放在参数列表里,要不然还需要在内部找到数列的最后一项,太烦。
这个make_tuple_indices_imp完工后是这个样子的:
template <std::size_t Sp, class IntTuple, std::size_t Ep> struct make_indices_imp; template <std::size_t Sp, std::size_t... Indices, std::size_t Ep> struct make_indices_imp<Sp, tuple_indices<Indices...>, Ep> { typedef typename make_indices_imp<Sp+1, tuple_indices<Indices..., Sp>, Ep>::type type; }; template <std::size_t Ep, std::size_t... Indices> struct make_indices_imp<Ep, tuple_indices<Indices...>, Ep> { typedef tuple_indices<Indices...> type; };
可以看到有三个版本,第一个是泛化形式,第二个是递归中间结果,第三个是递归终止条件(Sp==Ep)
然后我们就可以把make_tuple_indices写出来了:
template <std::size_t Ep, std::size_t Sp=0> struct make_tuple_indices { typedef typename make_indices_imp<Sp, tuple_indices<>, Ep>::type type; };
注意为了方便起见,Ep在前面,Sp在后面,因为缺省参数必须在最后(没办法C++就是这么规定的)
invoke可以很简单,就是把所有东西都forward过去调f:
template <class Fp, class... Args> inline auto invoke(Fp&& f, Args&&... args) -> decltype(std::forward<Fp>(f)(std::forward<Args>(args)...)) { return std::forward<Fp>(f)(std::forward<Args>(args)...); }
当然,为了应付不同的callable,比如成员函数指针,或者有operator()的类,或者lambda什么的,我们可能还需要几个特化,不过这些都不重要,这里就不说了。
6、有了上面这些东西,之前的thread_data::run/run2就能运转了,它能通过make_tuple_indices和invoke解包tuple并调用f(args...)。
我们已经有了run,之所以需要再定义一个run2,是因为一条可能很多人猛一下想不起来的C++语法规定——member function template不能是虚函数。
Indices是一个template type,只能用一个template function接收,所以我们需要把run和run2拆开,run作为继承下来的虚函数做入口,run2接收Indices并用之前提到的方法调用f(args...)。
结论:C++写这种东西神烦,要是有其它选择我立马叛逃(Rust目前还不够诱惑,等等再说)