The first idea is the following: assume that we have a class TestClass
class TestClass { public: void test_function (int i, double d); };
struct MemFunData { typedef void (TestClass::*MemFunPtr) (int, double); MemFunPtr mem_fun_ptr; TestClass *object; int arg1; double arg2; };
We further need a function that satisfies the signature required by the operating systems (or ACE, respectively), see Code Sample 1, and that can call the member function if we pass it an object of type MemFunData:
void * start_thread (void *arg_ptr) { // first reinterpret the void* as a // pointer to the object which // encapsulates the arguments // and addresses: MemFunData *mem_fun_data = reinterpret_cast<MemFunData *>(arg_ptr); // then call the member function: (mem_fun_data->object) ->*(mem_fun_data->mem_fun_ptr) (mem_fun_data->arg1, mem_fun_data->arg2); // since the function does not return // a value, we do so ourselves: return 0; };
We can then perform the desired call using the following sequence of commands:
MemFunData mem_fun_data; mem_fun_data.mem_fun_ptr = &TestClass::test_function; mem_fun_data.object = &test_object; mem_fun_data.arg1 = 1; mem_fun_data.arg2 = 3.1415926; ACE_Thread_Manager::spawn (&start_thread, (void*)&mem_fun_data);ACE_Thread_Manager::spawn is the function from ACE that actually calls the operating system and tells it to create a new thread and call the function which it is given as first parameter (here: start_thread) with the parameter which is given as second parameter. start_thread, when called, will then get the address of the function which we wanted to call from its parameter, and call it with the values we wanted as arguments.
In practice, this would mean that we needed a structure like MemFunData and a function like start_thread for each class TestClass and all functions test_function with different signatures. This is clearly not feasible in practice and places an inappropriate burden on the programmer who wants to use multiple threads in his program. Fortunately, C++ offers an elegant way for this problem, in the form of templates: we first define a data type which encapsulates address and arguments for all binary functions:
template <typename Class, typename Arg1, typename Arg2> struct MemFunData { typedef void (Class::*MemFunPtr) (Arg1, Arg2); MemFunPtr mem_fun_ptr; Class *object; Arg1 arg1; Arg2 arg2; };
template <typename Class, typename Arg1, typename Arg2> void * start_thread (void *arg_ptr) { MemFunData<Class,Arg1,Arg2> *mem_fun_data = reinterpret_cast<MemFunData<Class,Arg1,Arg2>*>(arg_ptr); (mem_fun_data->object) ->*(mem_fun_data->mem_fun_ptr) (mem_fun_data->arg1, mem_fun_data->arg2); return 0; };
MemFunData<TestClass,int,double> mem_fun_data; mem_fun_data.mem_fun_ptr = &TestClass::test_function; mem_fun_data.object = &test_object; mem_fun_data.arg1 = 1; mem_fun_data.arg2 = 3.1415926; ACE_Thread_Manager::spawn (&start_thread<TestClass,int,double>, (void*)&mem_fun_data);Here we first create an object which is suitable to encapsulate the parameters of a binary function that is a member function of the TestClass class and takes an integer and a double. Then we start the thread using the correct trampoline function. It is the user's responsibility to choose the correct trampoline function (i.e. to specify the correct template parameters) since the compiler only sees a void* and cannot do any type checking.
We can further simplify the process and remove the user responsibility by defining the following class and function:
class ThreadManager : public ACE_Thread_Manager { public: template <typename Class, typename Arg1, typename Arg2> static void spawn (MemFunData<Class,Arg1,Arg2> &MemFunData) { ACE_Thread_Manager::spawn (&start_thread<Class,Arg1,Arg2>, (void*)&MemFunData); }; };
ThreadManager::spawn (mem_fun_data);and the compiler will figure out which the right trampoline function is, since it knows the data type of mem_fun_data and therefore the values of the template parameters in the ThreadManager:: spawn function.
The way described above is basically the way which is used in deal.II version 3.0. Some care has to be paid to details, however. In particular, C++ functions often pass references as arguments, which however are not assignable after initialization. Therefore, the MemFunData class needs to have a constructor, and arguments must be set through it. Assume, for example, TestClass had a second member function
void f (int &i, double &d);Then, we would have to use MemFunData<TestClass,int&,doubleSPMamp;>, which in a form without templates would look like this:
struct MemFunData { typedef void (TestClass::*MemFunPtr) (int &, double &); MemFunPtr mem_fun_ptr; TestClass *object; int &arg1; double &arg2; };The compiler would require us to initialize the references to the two parameters at construction time of the MemFunData object, since it is not possible in C++ to change to which object a reference points to after initialization. Adding a constructor to the MemFunData class would then enable us to write
int i = 1; double d = 3.1415926; MemFunData<TestClass,int&,double&> mem_fun_data (&test_object, i, d, &TestClass::f);Non-reference arguments could then still be changed after construction. For historical reasons, the pointer to the member function is passed as last parameter here.
The last point is that this interface is only usable for functions with two parameters. Basically, the whole process has to be reiterated for any number of parameters which we want to support. In deal.II, we therefore have classes MemFunData0 through MemFunData10, corresponding to member function that do not take parameters through functions that take ten parameters. Equivalently, we need the respective number of trampoline functions.
Additional thoughts need to be taken on virtual member functions and constant functions. While the first are handled by the compiler (member function pointers can also be to virtual functions, without explicitly stating so), the latter can be achieved by writing MemFunData<const TestClass,int,double>, which would be the correct object if we had declared test_function constant.
Finally we note that it is often the case that one member function starts a new thread by calling another member function of the same object. Thus, the declaration most often used is the following:
MemFunData<TestClass,int&,double&> mem_fun_data (this, 1, 3.1415926, &TestClass::f);Here, instead of an arbitrary test_object, the present object is used, which is represented by the this pointer.