Why is a cast of uintptr_t used here?

229 views Asked by At

Here it is, in the context of pthread.h and stdint.h:

struct arguments {
    uint32_t threads;
    uint32_t size;
};

void *run_v1(void *arg) {
    uint32_t thread = (uintptr_t) arg;
    for (uint32_t j = 0; j < arguments.size; ++j) {
        size_t global_index = get_global_index(thread, j);
        char *string = get_string(global_index);
        hash_table_v1_add_entry(hash_table_v1, string, global_index);
    }
    return NULL;
}

...
int main () {
    ...
    for (uintptr_t i = 0; i < arguments.threads; ++i) {
        int err = pthread_create(&threads[i], NULL, run_v1, (void*) i);
        if (err != 0) {
            printf("pthread_create returned %d\n", err);
            return err;
        }
    }
   ...
}

This is my professor's code, I read the specification here:

The following type designates an unsigned integer type with the property that any valid pointer to void can be converted to this type, then converted back to a pointer to void, and the result will compare equal to the original pointer: uintptr_t

Why is the cast to this type necessary, rather than passing in a uint32_t and casting to itself? Is this undefined behavior?

3

There are 3 answers

5
Ted Lyngmo On

Why is the cast to this type necessary, rather than a cast to uint32_t?

It is not, but if uint32_t is smaller than uintptr_t you may get a warning about "cast from pointer to integer of different size". uintptr_t on the other hand is defined to be able to store pointer values as integers.

When the cast to uintptr_t is done you may still get a warning about "implicit conversion loses integer precision", so if what you've stored in the void* was actually an uint32_t to start with, add a cast to not get that potential warning:

uint32_t thread = (uint32_t)(uintptr_t)arg;

However, I suggest sending in the value via a pointer and then you wouldn't need any explicit casts:

void *run(void *arg) {
    uint32_t *ptr = arg;
    uint32_t thread = *ptr;
    ...
    return NULL;
}

uint32_t value;
pthread_create(..., &value);

A more elaborate example of making use of passing a pointer which is easy to extend if your thread needs more data than a single uint32_t could look like this:

#include <pthread.h>
#include <stdint.h>
#include <stddef.h>

#define SIZE(x) (sizeof (x) / sizeof *(x))

typedef struct {
    uint32_t value;
    // other data that the thread needs can be added here
} thread_data;

void *run(void *arg) {
    thread_data *data = arg;
    // work with data here
    return NULL;
}

int main() {
    pthread_t th[10];
    thread_data data[SIZE(th)];
    
    for(int i = 0; i < SIZE(th); ++i) {
        // fill data[i] with values to work with
        pthread_create(&th[i], NULL, run, &data[i]);
    }

    // ... join etc ...
}
6
AudioBubble On

The cast guarantees that the pointer be converted to an unsigned integer type in a reversible way.

In case this type is larger than a uint32_t, a compiler warning will be raised.

A direct cast to uint32_t might hide the fact that the conversion is not reversible. (Though, honestly, I cannot think of a implementation causing that.)

5
John Bollinger On

Why is the cast to this type necessary,

It is not.

rather than passing in a uint32_t and casting to itself?

That's what I would do.

Is this undefined behavior?

Maybe. But it definitely relies on implementation-defined behavior.


There are both general principles and problem-specific ones to consider here.

The most relevant general principle is the definition of uintptr_t, which you quoted. It tells you that uintptr_t can represent a distinct value corresponding to each distinct, valid void * value, so you can be confident that converting a void * to type uintptr_t will not produce a loss of fidelity. In general, then, if you want to represent an object pointer as an integer, uintptr_t is the integer type to choose.

It is relatively common to conclude that uintptr_t must be the same size as a void *, but although that's often true, the language spec places no such requirement. Since uintptr_t needs only to provide distinct representations for valid pointer values, and also because distinct void * bit patterns don't have to represent distinct addresses, uintptr_t could conceivably be smaller than void *. On the other hand, it can fulfill its role just fine if it is larger than void *.

Moreover, the language spec requires that you can round-trip pointers through type uintptr_t, but it does not require that you can round-trip any variety of integer through a pointer. The results of most integer-to-pointer conversions are implementation defined. That is, given this ...

    uintptr_t x;
    // .. assign a value to x ...

... the language spec allows this to print "unequal":

    if (x == (uintptr_t)(void *) x) {
        puts("equal");
    } else {
        puts("unequal");
    }

But in this specific case,

  • the upper bound on the values to be conveyed is read from an object of type uint32_t, and therefore all values to be conveyed are representable by that type; and

  • the program is assuming a C implementation in which the integer --> pointer --> integer transit reproduces the original value for all the integer values to be conveyed.

Under these circumstances, language semantics present no reason to prefer uintptr_t over uint32_t as the integer type involved. That is, if the code presented works correctly, then a version in which uinptr_t is replaced replaced with uint32_t must also work correctly. And I find the latter alternative cleaner and clearer.