2

A "typical" approach to using pthread with a member function of a class in C++ would be to use inheritance (like suggested here https://stackoverflow.com/a/1151615/157344 ). But why not something like this:

#include <pthread.h>

template <class T, void * (T::*thread)()>
class Thread
{
public:
    int create(T *that) { return pthread_create(&_handle, nullptr, _trampoline, that); };
    pthread_t getHandle() const { return _handle; };

private:
    static void * _trampoline(void *that) { return (static_cast<T *>(that)->*thread)(); };

    pthread_t _handle;
};

It can be used like that:

class SomeClassWithThread
{
public:
    int initialize() { return _thread.create(this); };

private:
    void * _threadFunction();
    Thread<SomeClassWithThread, &SomeClassWithThread::_threadFunction> _thread;
};

It has an advantage of NOT using virtual functions, so no vtable and less RAM used (I'm developing that for an MCU, not for a PC, so RAM usage matters). It also doesn't need virtual destructor.

Moreover I think it makes more sense, because a typical object rather HAS-A thread (composition), than IS-A thread (inheritance), right? (;

Is there any flaw with such design, as I haven't seen it suggested anywhere, as opposed to inheritance method? You'd definetely get a copy of _trampoline() for each instantiation but that's not much different from a virtual function call in inheritance version... I hope that create() and getHandle() would be inlined, as there's no reason not to...

Community
  • 1
  • 1
Freddie Chopin
  • 8,440
  • 2
  • 28
  • 58
  • In my experience, the accepted answer of the question you linked to is much more typical than the inheritance-based version. – molbdnilo Jan 18 '13 at 13:03
  • @molbdnilo - certainly, but that's not a "generic" or "automatic" way to do this - it's just providing such trampoline() by hand for each class you'd like to enhance with a pthread. I think my template approach is closer to such version anyway - it's just a really fancy macro that does the same thing (; Or at least I think it does - I'm a newbie in these areas... – Freddie Chopin Jan 18 '13 at 13:26
  • You might want to add a destructor to your Thread class, so that the thread is stopped when the class is destroyed. Nitpick: it's a bad idea to use leading underscores in variable names; these are resrved for the compiler. – Simon Elliott Jan 18 '13 at 13:59
  • @FreddieChopin Well, you did ask what was typical ;-) I think your solution is better than the typical because it hides pthread specifics. Also, inheritance is severely overrated (and arbitrarily limits you to one thread per object). – molbdnilo Jan 18 '13 at 14:08
  • @Simon Elliott - the destructor is not there as it's for an MCU (ARM Cortex-M3) and there are several reasons, first - the objects that have these threads are global and are never destroyed (I didn't find an elegant way to make them local without wasting RAM just for "good style" and passing a reference of the object to each object that needs it...), second - the RTOS that I'm using (NuttX) doesn't really support deleting (cancelling) tasks - it's just a small MCU (; As for the underscores i think it's just a style debate (; – Freddie Chopin Jan 18 '13 at 14:21
  • @molbdnilo - I've also thought about that just a while ago - with the template approach you can have as many threads inside a single object as you want (; – Freddie Chopin Jan 18 '13 at 14:23
  • 1
    I think there's a bug in this: your use of "this" in create() is the address of the helper class and not the address of the class you want to use as a client. You example works by accident because your helper class is the first member. – Simon Elliott Jan 18 '13 at 15:01
  • @Simon Elliott - thx for spotting, fixed that in the original question – Freddie Chopin Jan 18 '13 at 15:15
  • 1
    @SimonElliott, those names are not reserved. `_This` is a reserved name, and so is `__this`, but `_this` is not except in the global namespace (and none of those names are in the global namespace) – Jonathan Wakely Jan 19 '13 at 22:25

3 Answers3

1

This resolves the problem with the "this" address

#include <pthread.h>

template <class T, void * (T::*thread)()>
class Thread
{
public:
    int create(T* passedThis) { return pthread_create(&_handle, nullptr, _trampoline, passedThis); };
    pthread_t getHandle() const { return _handle; };

private:
    static void * _trampoline(void *that) { return (static_cast<T*>(that)->*thread)(); };

    pthread_t _handle;
};

Updated usage:

class SomeClassWithThread
{
public:
    int initialize() { return _thread.create(this); };

private:
    void * _threadFunction();
    Thread<SomeClassWithThread, &SomeClassWithThread::_threadFunction> _thread;
};
Simon Elliott
  • 2,087
  • 4
  • 30
  • 39
  • I've changed it to the same semantics just now - good catch! Indeed it was working when the thread object was first and failed if not (; – Freddie Chopin Jan 18 '13 at 15:14
1

Your Thread::_trampoline method doesn't have extern "C" linkage so, although this may work in practise, it isn't correct. Since you can't give the correct linkage to template functions, there isn't an easy way to automate this unless you allow macros.

Moreover I think it makes more sense, because a typical object rather HAS-A thread (composition), than IS-A thread (inheritance), right? (;

Nope, that depends on your model.

  • active objects are frequently bound tightly to a single thread (so you can consider than an instance IS-A concurrently-executing process)
  • with task queues (or thread pools) it's frequently the task that is the object, whereas the thread is associated with some scheduling logic (the queue/pool itself may be an object too of course, but that doesn't seem to be what you're suggesting)

Honestly, if you're creating so many different top-level thread functions that you're worried about the memory taken up by vtables, I suspect your design is wrong in the first place.

Useless
  • 64,155
  • 6
  • 88
  • 132
  • about _trampoline() not being extern "C" - why would that change anything, calling convention is known, I just don't really know what difference that would make... as for your last remark about the design being wrong it doesn't seem like a constructive comment - there's nothing wrong in saving a few bytes if you don't need to do anything special to achieve that and the solution with templates is as good as the one with inheritance... As for the middle thing (compositon vs. inheritance) you can easily create an object with one such Thread and inherit it - voila (; – Freddie Chopin Jan 18 '13 at 16:10
  • If you know that static C++ linkage functions are compatible with C linkage functions in your ABI, good for you. I don't know what your platform guarantees, so I'm just pointing out it isn't portable. The rest only follows if the template solution _is_ correct, which we've established I don't know in general. As for the design, that's probably a fair comment ... I just don't have enough context to suggest a better approach. – Useless Jan 18 '13 at 16:15
  • Don't get me wrong - I'm not saying you are mistaken, but I just never heard that calling static member functions is any different from calling normal C function (excluding name mangling, but that does not matter here), so I'm just curious - you never stop learning (; As a sidenote - the inheritance-method also uses static member function as a trampoline, so this is a "flaw" of both methods. – Freddie Chopin Jan 18 '13 at 16:40
  • True, but if you use an ABC with the inheritance method, you can easily have a single non-templated, non-member `extern "C"` trampoline function to do the initial cast from `void *` and call the virtual. – Useless Jan 18 '13 at 17:37
1

As Useless mentioned in his answer, strictly speaking the thread function called by the pthread library needs to be extern "C". While a static member function works in nearly all cases, from a language lawyer point of view and at least one real life situation, it's not correct. See https://stackoverflow.com/a/2068048/12711 for details.

However, you can have an extern "C" function provide the interface between the pthread library and your class template, but it does seem to require a tiny bit of overhead:

#include <pthread.h>

struct trampoline_ctx
{
    void* (*trampoline)(void*);
    void* obj;
};

extern "C"
void* trampoline_c(void* ctx)
{
    struct trampoline_ctx* t = static_cast<struct trampoline_ctx*>(ctx);

    return (t->trampoline)(t->obj);
}


template <class T, void * (T::*thread)()>
class Thread
{
public:
    int create(T *that) { 
        ctx.trampoline = _trampoline;
        ctx.obj = that;
        return pthread_create(&_handle, nullptr, trampoline_c, &ctx); 
    };
    pthread_t getHandle() const { return _handle; };

private:
    static void * _trampoline(void *that) { return (static_cast<T *>(that)->*thread)(); };

    pthread_t _handle;
    struct trampoline_ctx ctx;
};

I agree that composition rather than inheritance is probably a better model for threading most of the time.

Of course, remember that C++11 provides std::thread which is a templated, non-inheritance design. And see boost::thread if C++11 isn't an option.

Community
  • 1
  • 1
Michael Burr
  • 333,147
  • 50
  • 533
  • 760
  • The std::thread solution won't work for me - first of all the RTOS I'm using makes it impossible to use stdlibc++ from the toolchain (yes, I also agree it's stupid), second of all the C++11 design is probably a big overkill for my 24kB or RAM and 24MHz chip, third of all - this implementation is OS dependant and there's no real OS here (; As for your solution - using dynamic alocation this overhead could be temporary (all 3 pointers are not needed when the thread is started), but that would bring heap to the picture (; there's always a tradeof (; – Freddie Chopin Jan 20 '13 at 10:58
  • While I realize the above statement is correct based on what the c++ standard says. This is one of the few cases where you can get away with not using extern "C" when mixing c and C++ for most compilers, especially gnu compliant compilers(g++, clang++). The reason for this, I think, is that most C++ compilers find the function name during compile time and plug the pointer value in. I think the problem comes in that some compilers actually pass the name in to pthread_create which causes a crash since that name doesn't exist. If anybody has anymore detailed information on this would love to hear – Zachary Kraus Aug 27 '14 at 01:05