During a conversation at the March Java and C/C++ Conference I promised not to inflict another article about threads on Overload readers. It is time to move on to other topics. However, Kevlin Henney raised some issues that are well worth discussing. So, please bear with me one more time. I shall be brief.
In Part 2 I presented a thread handle class with an assignment operator implemented by destroying and re-constructing the handle object. This is not a good idea. Kevlin tells me that the reasons are explained in Herb Sutter's book, Exceptional C++ [Sutter], and in Ruminations on C++ [Koenig-] by Andrew Koenig and Barbara Moo. I have not yet been able to consult Ruminations, but Herb's Guru of the Week item entitled Object Lifetimes - Part I makes three points about this technique:
The technique works except...
... when the constructor can throw an exception or ...
... the class is used as a base class.
(You will find this under item 22 on the PeerDirect Web site [peerdirect] or item 40 in Exceptional C++.)
I will say in my defence that my handle's copy constructor does not throw exceptions and the handle class was not intended to be used as a base class. Nevertheless, as Herb puts it, this is a "bad habit".
Here is a better implementation of the thread handle class from the earlier article.
Kevlin also suggested a mechanism to make the thread library more general and easier to use. The idea is to provide the thread handle class with a template constructor.
Client code can run any suitable function or function object in a separate thread by passing it to this constructor. In this case, a "suitable" object is one that satisfies the following requirement:
Given an object, fn, the expression fn() must be well formed and of type int.
In particular, the fn object may be a pointer to a non-member function, a pointer to a static member function or any function object with an int operator()() member. Note that suitable function objects do not have to be derived from thread::function.
So, for example, these code fragments are valid:
I had intended to include a template constructor in the sample code I presented in Part 2 of this series of articles. This was to be my alternative to Allan Kelly's class template design [Kelly31], [Kelly33]. Unfortunately, I discovered lots of other issues along the way and left myself no time to work out how to implement the template constructor. So, when Kevlin suggested using the External Polymorphism pattern, I had no excuse for leaving this loose end untied. This was, after all, the reason for starting these articles in the first place.
The External Polymorphism design pattern describes a hierarchy of polymorphic classes whose behaviour is determined by non-virtual functions. It is particularly appropriate here because my thread implementation relies on polymorphic function objects derived from the thread::function class, but the behaviour of these function objects may be provided by non-member functions or non-virtual function call operators.
The key to the External Polymorphism pattern is the generation of adapter classes for each non-virtual function. For the thread library these will be function adapters and they can be conveniently defined by a single class template, as shown in Figure 5. In this case I have reverted to the more usual method of storing the function-like object by value instead of by reference.
The External Polymorphism approach does have one drawback - rather more scaffolding is needed to ensure that the thread implementation is hidden from clients. The extra complexity arises because the definition of the template constructor must be available at the point of use, i.e. in the client code. In this case, the handle's constructor implementation creates the function adapter. Since this is also a template, its definition must also be available at the point of use. But, if we wish to hide the platform-specific parts of the implementation, the class that encapsulates them must remain an incomplete type in the header file. So the implementation details must be split across two classes (body and function_adapter<F>) instead of one.
Hiding the platform-specific implementation details also makes it necessary to use an auxiliary function (make_body) to create the thread body.
Overload Journal #38 - Jul 2000 + Programming Topics
|Browse in :||
All > Topics > Programming (768)
Any of these categories - All of these categories