It is common for C-style APIs that take a function pointer as a callback to also take a pointer-sized argument as a "context", that is passed into the callback so that information can be passed from the call-site to the invocation of the callback. For example, pthread_create:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Here, arg is the "context".
Recently, I came across a situation where I wanted to pass an integer into such a function. I obviously didn't want to actually pass a pointer to an integer because I would have to dynamically allocate it to guarantee the lifetime.
So my solution was to reinterpret_cast the int to void*, and then back to int in the callback. However, I later learnt that this is not portable: Does reinterpret_casting an integral to a pointer type and back yield the same value?
If that's the case, what is the solution?
To avoid this issue, should such APIs take a uintptr_t instead of void*?
I obviously didn't want to actually pass a pointer to an integer because I would have to dynamically allocate it to guarantee the lifetime.
I understand that your question isn't just about pthread_create so I'll attempt to answer in a broader sense. However, you've also focused specifically upon pthread_create, giving an example, so I feel the need to answer that question too.
In the context of pthread_create, your C++ code should use a C++ idiom such as std::thread. If you are going to use the C idiom, to provide a nice balance of portability, cleanliness and maintainability in the context of pthread_create you should dynamically allocate this object! However, there are alternatives. Avoiding dynamic allocation seems like a premature optimisation; it's the simplest solution (aside from using the C++ idiom). We could drivel on all day avoiding the simplest solutions until we reach the brink of insanity, but that wouldn't be too useful, would it?
... what is the solution?
In the context of other APIs, the sky's the limit. C++ has a marvelous set of features that make life easier, yet don't introduce noticeable overhead or complexity. We should try to keep maintainability in mind when we're designing APIs...
In the context of pthread_create, there are two almost insane alternatives:
pthread_create call by using pthread_rwlock_t or pthread_mutex_t to ensure the object remains alive for long enough. However, this seems like more work than using an object with dynamic storage duration. Do you notice how we're slowly reaching towards insanity?What reason do you have to avoid the saner option, std::thread?
... should such APIs take a
uintptr_tinstead ofvoid*?
I suppose that depends upon the APIs. It's a decision during the design phase. In the context of pthread_create, that API is a part of POSIX. I want to make it clear that as the POSIX world currently stands, the only functions that can be called by pthread_create must take a void * as an argument and return a void *. However, the POSIX standard doesn't seem to require that these pointers point at anything.
In both the worlds of C++ and POSIX, the uintptr_t type is optional where-as the void * type is mandatory. Any APIs that want to utilise uintptr_t should do so with this optionality in mind; <pthread.h> doesn't seem optional in the POSIX.1-2008 world, and such a change could break portability as we'll soon explore, so I wouldn't expect a change to the POSIX pthread API.
If uintptr_t does exist, there are guarantees that a conversion from uintptr_t to void * and back to uintptr_t will yield the same value, so pthread_create(..., fubar, (void *) 42) (or similar using reinterpret_cast) can be well-defined providing uintptr_t exists and fubar performs the inverse conversion (i.e. (uintptr_t) context will equal 42).
Similarly, a conversion from void *(*)(uintptr_t) to void *(*)(void *) (i.e. in your call to pthread_create), and back to void *(*)(uintptr_t) produces a function pointer which can be invoked. However, invoking a function as the wrong type produces undefined behaviour! The C standard (which the POSIX standard adopts) is actually better at explaining this than I, so here's an extract from C11/6.3.2.3p8:
A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the referenced type, the behavior is undefined.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With