Supporting Threads in Standard C++ (Addendum)

Supporting Threads in Standard C++ (Addendum)

By Phil Bass

Overload, 8(38):, July 2000


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.

Bugs

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:

  1. The technique works except...

  2. ... when the constructor can throw an exception or ...

  3. ... 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.

Figure 1 - Handle Interface.

// A thread handle that shares ownership 
// of a 'body'
class handle 
{
public:
// The default four

  handle(function&);
  ~handle() throw();

  handle (const handle&);
  handle& operator= (const handle&);

// ... thread functions ...
private:
  void attach (body*);
  void detach () throw();

  body* shared_body;
};

Figure 2 - Handle Implementation.

inline void handle::attach 
                    (body* new_body){
  shared_body = new_body;
  ++shared_body->handle_count;
}

inline void handle::detach() throw(){
  if (-shared_body->handle_count == 0)
    delete shared_body;
}

handle::handle (function& fn) 
          {attach(new body(fn));}

handle::~handle() throw() {detach();}

thread::handle::handle 
          (const handle& old_handle){
  attach(old_handle.shared_body);
}

handle& handle::operator= 
          (const handle& other_handle){
  detach();
  attach(other_handle.shared_body);

  return *this;
}

Improvements

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.

Figure 3 - Thread handle class with template constructor.

class thread::handle
{
public:
  template <typename function> 
    handle (function);
  . . .
};

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:

Example 3: function object

// Example 1: non-member function
int some_function();

int main(){
  thread::handle handle(some_function);
}
// Example 2: static member function
struct some_class {
  static int some_function();
};

int main() {
  thread::handle 
  handle(some_class::some_function);
}

Figure 4 - Using the template constructor.

struct function_object {
  int operator()();
};

function_object some_function_object;

int main() {
  thread::handle handle(some_function_object);
}

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.

Figure 5 - The template constructor implementation.

// thread.h
namespace thread  {
  struct function;
  template <typename F> 
    class function_adapter;
  class handle;
  class body;
}

struct thread::function {
  virtual ~function() {}
  virtual int operator() () = 0;
};

template <typename F>
  class thread::function_adapter 
            : public thread::function {
public:
  function_adapter (F f) : fn(f) {}
  virtual int operator()() {return fn();}
private:
  F fn;
};

class thread::handle {
public:
  template <typename function>
    handle (function);
  . . .
private:
  body* make_body (thread::function*);
  body* shared_body;
};

template <typename function>
  thread::handle::handle (function fn)
  : shared_body(make_body(new
     function_adapter<function>(fn))){}

Hiding the platform-specific implementation details also makes it necessary to use an auxiliary function ( make_body ) to create the thread body.

References

[Sutter] Exceptional C++ by Herb Sutter, ISBN 0-201-61562-2.

[Koenig-] Ruminations on C++ by Andrew Koenig and Barbara Moo, ISBN 0-201-42339-1.

[peerdirect] http://www.peerdirect.com

[Kelly31] Overload, No. 31 , April 1999, Using Templates to Handle Multi-threading by Allan Kelly.

[Kelly33] Overload, No. 33 , August 1999, More Threading with Templates by Allan Kelly.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.