What Is Uintptr_T Data Type

What is uintptr_t data type

uintptr_t is an unsigned integer type that is capable of storing a data pointer. Which typically means that it's the same size as a pointer.

It is optionally defined in C++11 and later standards.

A common reason to want an integer type that can hold an architecture's pointer type is to perform integer-specific operations on a pointer, or to obscure the type of a pointer by providing it as an integer "handle".

uintptr_t and intptr_t in C language

The XOR linked list method is a hack to construct a linked list that can be navigated in both directions using the space for a single pointer. The trick is to store the XOR of addresses of the next and previous items in the link member, converting these addresses as uintptr_t (or intptr_t) values, to perform bitwise exclusive or on integers of the appropriate size and store this info as an integer:

struct item {
uintptr_t link;
int data; // item payload. Can be any number of members of any type
};

The list can be traversed in both directions, provided you know the address of the previous (or the next) item:

struct item *get_link(struct item *p, const struct item *previous) {
return (struct item *)(p->link ^ (uintptr_t)previous);
}

To avoid a warning on alignement issues, you may need to add an extra cast as:

return (struct item *)(void *)(p->link ^ (uintptr_t)previous);

uintptr_t is an integer type that is specified as having the same size as void *, hence can contain all the information from any data pointer. Converting a data pointer to uintptr_t and back with casts should yield the same pointer.

intptr_t is the corresponding signed type, which is of little use per se.

The XOR linked list hack is mostly of historical interest today. The only advantage is a small size saving that is hardly worth the added complication. It is much better to use regular doubly linked lists if you need to scan the list in both directions. The scan with this trick requires keeping the pointer to both the current item an the previous one in the direction of traversal, whereas regular doubly linked lists can be handled with a single pointer, hence can be manipulated and/or shared in a much simpler fashion.

Here is a sample implementation:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

struct item {
uintptr_t link;
int data; // item payload. Can be any number of members of any type
};

struct xor_list {
struct item *head;
struct item *tail;
};

struct item *get_link(struct item *ip, const struct item *previous) {
return (struct item *)(ip->link ^ (uintptr_t)previous);
}

struct item *get_next(struct item *ip, struct item **previous) {
struct item *next = get_link(ip, *previous);
*previous = ip;
return next;
}

uintptr_t make_link(struct item *prev, const struct item *next) {
return (uintptr_t)prev ^ (uintptr_t)next;
}

struct item *add_item(struct xor_list *lp, int data) {
struct item *ip = malloc(sizeof(*ip));
if (ip) {
struct item *tail = lp->tail;
ip->data = data;
if (tail) {
struct item *prev = get_link(lp->tail, NULL);
ip->link = make_link(tail, NULL);
tail->link = make_link(prev, ip);
lp->tail = ip;
} else {
ip->link = make_link(NULL, NULL);
lp->head = lp->tail = ip;
}
}
return ip;
}

int main() {
struct xor_list list = { NULL, NULL };
struct item *ip, *prev;

add_item(&list, 1);
add_item(&list, 2);
add_item(&list, 3);
add_item(&list, 4);
add_item(&list, 5);

printf("traversing from head to tail:");
for (prev = NULL, ip = list.head; ip; ip = get_next(ip, &prev)) {
printf(" %d", ip->data);
}
printf("\n");

printf("traversing from tail to head:");
for (prev = NULL, ip = list.tail; ip; ip = get_next(ip, &prev)) {
printf(" %d", ip->data);
}
printf("\n");
return 0;
}

Does uintptr_t store the address of a pointer or its value?

It's an integral value that represents the address that foo points to.

If you were to write it as

uintptr_t bar = (uintptr_t)(&foo);

Then it would be the address of foo itself.

If a `uintptr_t` can be used to store both pointers and numbers, why can't a `void*` do the same?

Even if void * values are represented as numbers, that does not mean the compiler handles them as it does numbers.

A uintptr_t is a number; C 2018 7.20.1.4 1 says it designates an unsigned integer type. So it behaves like other unsigned integer types: You can put any number within its range and get the same number back (and you can do arithmetic with it). The paragraph further says any valid void * can be converted to uintptr_t and that converting it back will produce the original pointer (or something equivalent, such as a pointer to the same place but with a different representation). So you can store pointers in uintptr_t objects.

However, the C standard does not say there is a range of numbers you can put into void * and get them back. 6.3.2.3 5 says that when an integer is converted to a pointer type, the result is implementation-defined (except that converting a constant zero to void * yields a null pointer, per 6.3.2.3 3). 6.3.2.3 6 says when you convert a pointer to an integer, the result is implementation-defined. (7.20.1.4 overrides this when the number is a uintptr_t that came from a pointer originally.)

So, if you store a number in a void *, how do you know it will work? The C standard does not guarantee to you that it will work. You would need some documentation for the compiler that says it will work.

C - Why cast to uintptr_t vs char* when doing pointer arithmetic

In comments user R.. points out that the following is likely incorrect if the addresses the code is dealing with are not valid within the current process. I've asked the OP for clarification.

Do not use uintptr_t for pointer arithmetic if you care about the portability of your code. uintptr_t is an integer type. Any arithmetic operations on it are integer arithmetic, not pointer arithmetic.

If you have a void* value and you want to add a byte offset to it, casting to char* is the correct approach.

It's likely that arithmetic on uintptr_t values will work the same way as char* arithmetic, but it absolutely is not guaranteed. The only guarantee that the C standard provides is that you can convert a void* value to uintptr_t and back again, and the result will compare equal to the original pointer value.

And the standard doesn't guarantee that uintptr_t exists. If there is no integer type wide enough to hold a converted pointer value without loss of information, the implementation just won't define uintptr_t.

I've actually worked on systems (Cray vector machines) where arithmetic on uintptr_t wouldn't necessarily work. The hardware had 64-bit words, with a machine address containing the address of a word. The Unix-like OS needed to support 8-bit bytes, so byte pointers (void*, char*) contained a word address with a 3-bit offset stored in the otherwise unused high-order 3 bits of the 64-bit word. Pointer/integer conversions simply copied the representation. The result was that adding 1 to a char* pointer would cause it to point to the next byte (with the offset handled in software), but converting to uintptr_t and adding 1 would cause it to point to the next word.

Bottom line: If you need pointer arithmetic, use pointer arithmetic. That's what it's for.

(Incidentally, gcc has an extension that permits pointer arithmetic on void*. Don't use it in portable code. It also causes some odd side effects, like sizeof (void) == 1.)

How to define a type that can hold a uintptr_t or a uint32_t without a union?

There's no need for the preprocessor to get such an alias. It's a simple use case for the standard library's type traits

using uintptr = std::conditional_t<(sizeof(uint32_t) > sizeof(uintptr_t)),
uint32_t, uintptr_t>;

don't quite get what is: uintptr_t value(astruct.val)

uintptr_t is an unsigned integer (at least) the size of a pointer.

It is not a native type though and you need to include <stdint.h> or <cstdint>

Really it is "evil" to cast pointers to ints and store them as integral variables but there is a lot of "legacy" code that does it and relies on it, and so you need an int size big enough to store it.

Storing them as integers allows you to do things you can do with ints "safely" but not with pointers, such as compare them when they are not part of the same range, in order to use them in memory-leak checkers, etc.

Why / when to use `intptr_t` for type-casting in C?

Here's the thing: on some platforms, int is the right size, but on others, long is the right size. How do you know which one is the one you should use? You don't. One might be right, but the standard makes no guarantees about which one it would be (if it is either). So the standard provides a type that is defined to be the correct size, regardless of what platform you're on. Where before you had to write:

#ifdef PLATFORM_A
typedef long intptr;
#else
typedef int intptr;
#endif

Now you just write:

#include <stdint.h>

And it covers so many more cases. Imagine specializing the snippet above for every single platform your code runs on.



Related Topics



Leave a reply



Submit