How do I determine the largest pointer size on my platform?
Asked Answered
I

2

8

In (C and) C++, pointers to different types don't necessarily have the same size. I would have hoped void * is necessarily the largest, but it seems not even this is actually guaranteed.

My question: How can I determine what the largest size of a pointer is on my (compilation target) platform?

Note: I mean any pointer, including pointers to class member functions; things you can get with the & operator. I don't mean entities which are "colloquially" known as pointers, i.e. not unique_ptr's or shared_ptr's and such.

Incestuous answered 18/4, 2019 at 12:16 Comment(7)
Do you want to include member function pointers in this?Aurelie
Do you also care about pointers to member functions void (Class::* func)(), and pointers to member data int Class::* mem?Jessalyn
Are you including smart pointer objects as pointers? std::unique_ptr, std::shared_ptr, and the old-and-broken std::auto_ptr.Abm
Maybe you should tell us what you actually want to do with this maximized out pointer size.Totem
Why do you need this information? Pointers should not be externalized or internalized, so what is the scenario where this could be useful?Ridenour
@MarekR: Not for anything immediate, but I used to think it was safe to type-erase any pointer by reinterpreting it as a void *, and now I know that's not true. So I'd at least like to be able to have an std::array<std::byte, max_size_of_any_ptr> to put type-erased pointers in.Incestuous
this question deserves more upvotesKurland
I
5

There are four completely unrelated classes of pointer types in the C++ language: object pointers, function pointers, non-static data member pointers, and non-static member function pointers. The term "pointer" generally only applies to object and function pointer types [basic.compound]/3:

[…] Except for pointers to static members, text referring to “pointers” does not apply to pointers to members. […]

Pointers and pointers to non-static members are actually treated as two completely separate kinds of compound types altogether [basic.compound]/1 (which makes sense since non-static member pointers are more like relative offsets and less like actual addresses).

Except for a conditionally-supported conversion between object and function pointers, the semantics of which (if supported at all) will be implementation-defined [expr.reinterpret.cast]/8, there is no way to convert between these four classes of pointer types.

However, the standard does specify interconvertibility amongst object pointers [expr.reinterpret.cast]/7, interconvertibility amongst function pointers [expr.reinterpret.cast]/6, interconvertiblity amongst data member pointers [expr.reinterpret.cast]/10.2, and interconvertibility amongst member function pointers [expr.reinterpret.cast]/10.1.

As a result, while there is no common pointer type that all other pointer types are related to in general, it is well-defined behavior to cast any object pointer to some arbitrary object pointer type and back. It is well-defined behavior to cast any function pointer to some arbitrary function pointer type and back. It is well-defined behavior to cast any data member pointer to some arbitrary data member pointer type and back. And it is well-defined behavior to cast any member function pointer to some arbitrary member function pointer type and back. And one thing all these different classes of pointer types have in common is that they're all object types [basic.types]/8.

While this does not strictly guarantee that, e.g., all member function pointer types are the same size, it does implicitly establish that any object of some member function pointer type can effectively be used to store any member function pointer value. There may still be member function pointer types larger than others, but they could not possibly hold more information than others since the standard requires that the conversion to and from any other member function pointer type must not lose information (the original value can always be restored). The same argument works analogously for all the other classes of pointer types.

Based on all this, I would argue that it's technically impossible to find "the largest pointer type" in standard C++. However, while it may technically be impossible to find the largest pointer type itself, based on the argument above, it is definitely possible to find an upper bound for the amount of storage needed to reliably store any value of pointer type. While those two are technically different things, in practice, the second one is most likely almost as good as the first (no reasonable compiler will just randomly add lots of padding bits to the value representation of some pointer type just because doing so is technically legal). At least I'm having a hard time imagining what else than store pointer values you could possibly be wanting to do with the kind of information you're asking for.

Using, for example

using generic_obj_ptr = void*;
using generic_fun_ptr = void (*)();

class dummy_t;
using generic_dat_mem_ptr = dummy_t dummy_t::*;
using generic_mem_fun_ptr = void (dummy_t::*)();

you can compute

auto obj_ptr_size = sizeof(generic_obj_ptr_t);
auto fun_ptr_size = sizeof(generic_fun_ptr_t);
auto dat_mem_ptr_size = sizeof(generic_dat_mem_ptr_t);
auto mem_fun_size = sizeof(generic_mem_fun_ptr_t);

auto max_ptr_size = std::max({ sizeof(generic_obj_ptr_t), sizeof(generic_fun_ptr_t), sizeof(generic_dat_mem_ptr_t), sizeof(generic_mem_fun_ptr_t) });
auto max_ptr_align = std::max({ alignof(generic_obj_ptr_t), alignof(generic_fun_ptr_t), alignof(generic_dat_mem_ptr_t), alignof(generic_mem_fun_ptr_t) });

or just use

using ptr_storage_t = std::aligned_union<0U, generic_obj_ptr_t, generic_fun_ptr_t, generic_dat_mem_ptr_t, generic_mem_fun_ptr_t>;

or even

using any_ptr_t = std::variant<generic_obj_ptr_t, generic_fun_ptr_t, generic_dat_mem_ptr_t, generic_mem_fun_ptr_t>;

or in its pure form:

using any_ptr_t = std::variant<void*, void (*)(), dummy_t dummy_t::*, void (dummy_t::*)()>;

as storage in which any object pointer value can be stored when cast to and from void*, any function pointer value can be stored when cast to and from void (*)(), any data member pointer can be stored when cast to and from dummy_t dummy_t::*, and any member function pointer can be stored when cast to and from void (dummy_t::*)().

play with it here

The task of wrapping this in a class that takes care of all the casting for storing arbitrary values of any pointer type (don't forget to deal with possible cv qualification), shall be left as an exercise for the reader, mainly because I would really like to sleep well tonight…

Inflate answered 18/4, 2019 at 16:16 Comment(1)
Thanks for making me aware of aligned_union. And +1 for an excellent answer.Incestuous
S
4

There are 3 different types of pointers, which can have a different size:

  • pointer-to-object
  • function pointer
  • member function pointer

A void * is guaranteed to be large enough to hold every pointer-to-object according to the C++17 Standard 6.9.2.5:

A pointer to cv-qualified ([basic.type.qualifier]) or cv-unqualified void can be used to point to objects of unknown type. Such a pointer shall be able to hold any object pointer. An object of type cv void* shall have the same representation and alignment requirements as cv char*.

class A;

typedef void (A::*a_func_ptr)(void);
typedef void (*func_ptr)(void);

size_t a = sizeof(a_func_ptr), b = sizeof(func_ptr), c = sizeof(void*);

std::cout << std::max(a, std::max(b, c)) << std::endl;

should do the job.

edit: The C++17 Standard 6.9.2.3 says

Except for pointers to static members, text referring to “pointers” does not apply to pointers to members.

So, the largest possible pointer is either a void * or a function pointer:

std::cout << std::max(sizeof(void*), sizeof(void(*)(void))) << std::endl;
Sledgehammer answered 18/4, 2019 at 12:21 Comment(16)
I think this question deserves a citation from the standard tbhNormative
@HolyBlackCat ah, you're right. I had thought that pointers to functions weren't "pointers" in strict sense, but I was wrong. Standard reference: eel.is/c++draft/basic.compound#3Extraction
not all member function pointers have the same size: #13876286Literalism
Just to add to the muddle: C++ added pointer to member; that can be a pointer to a member function or it can be a pointer to member data.Nalley
Also, a void* is not required to be large enough to hold a function pointer. There is no conversion from pointer-to-function to void*. The only conversion to void* is from pointer-to-object (or from a null pointer constant, but that's not pertinent here). See [conv.ptr]/2. Or follow the link in the question.Nalley
@PeteBecker I never said that a void * is large enough to hold a function pointer, only for a data pointer (or pointer-to-object). Should I change data pointer to pointer-to-object? Also the link in the question is for C, the question is for C++, we would not have this discussion in C.Sledgehammer
AFAIK if polymorphism is involved size of pointer to object can be doubled.Ridenour
@MarekR no, the Standard requires that a void* can hold any object pointer, so, sizeof(void*) is the size of the largest object pointer.Sledgehammer
@LightnessRacesinOrbit citations added.Sledgehammer
@MarekR Why would its size be doubled? Its value can change with casts (if you have multiple inheritance) - is that what you were thinking of?Normative
I remember I was using some compiler which behaved like this. I don't remember exact scenario. It was long time ego and I don't remember details, but I remember my surprise that size was not matching size of void*Ridenour
@MarekR Either your observation was buggy or your compiler was dangerously non-compliant! (Either because it was itself buggy, or because your observation predates the standard and thus the very possibility of compliance)Normative
When you wrote compiler was dangerously non-compliant then I'm starting suspecting it was Borland C++ Builder and it was pointer to method +10 years ago.Ridenour
@LightnessRacesinOrbit a member function pointer will typically be the size of two pointers because it has to also encode a potential offset that must be applied on the this pointer before the function pointed to can be called in case that polymorphism is involved. I guess that's most likely what Marek was thinking about…Inflate
@MichaelKenzel Why would it not just be a pointer to the function code? The this pointer is not part of the pointer-to-member, but is supplied at the callsite.Normative
@LightnessRacesinOrbit because there can be member function pointers that point to virtual member functions, in which case the function "pointer" actually has to be a vtable index. You can easily convince yourself, e.g., here. Note how pfun is a 16-Byte value that contains the vtable index rather than the address to the actual function. Also, note how pfa is a 16-Byte value that contains not only the address of A::fa but also the offset that must be applied to the this pointer when the function is invoked on an object of type CInflate

© 2022 - 2024 — McMap. All rights reserved.