9

Can anyone tell me why the internal representation of a nullptr assigned to a data member pointer of type Class::* is -1 for MSVC, clang and g++? For a 64-bit system (size_t)1 << 63 would be best, because if you use a nullptr member pointer that way you will touch kernel memory for sure and have a crash so this would be a nice debugging aid.

Is there a deeper reason behind -1?

Sample:

struct X
{
    int x, y;
};

using member_ptr = int X::*;

member_ptr f()
{
    return nullptr;
}

... results in the following binary with g++:

movq    $-1, %rax
ret
Bonita Montero
  • 2,817
  • 9
  • 22
  • It should not be -1 for msvc. It should be 0 regardless of 64 or 32 bit. – drescherjm May 19 '22 at 13:32
  • 1
    The C standard library allows a null pointer to be defined by a system for legacy purposes. I am very surprised you managed to find a case where that was actually the case. What type of system are you running on? – Locke May 19 '22 at 13:33
  • 1
    @drescherjm: I'm talking about _member_ pointers, these are internally just offsets, not absolute pointers. – Bonita Montero May 19 '22 at 13:34
  • 3
    @Locke: I'm not taling about C but C++. C hasn't any member-pointers. – Bonita Montero May 19 '22 at 13:35
  • Interesting. Maybe something we can look at on godbolt.org with a small example code. I don't look at the assembly very often these days. In the late 1990s I had to write asm for some medical imaging code to improve performance but these days I mainly use libraries. – drescherjm May 19 '22 at 13:41
  • Why -1? Because it's an invalid pointer for a member offset but 0 is valid for the implementation. In your example the value for `&X::x` is 0. – Mgetz May 19 '22 at 13:46
  • Of course it is an invalid pointer, but the value I suggested is also invalid and would help debugging. – Bonita Montero May 19 '22 at 13:47
  • 1
    @Mgetz OK but `size_t(1) << 63` would also be an invalid pointer, and accessing it would lead to a nice crash, as the question states. By contrast, `-1` is an offset that’s almost guaranteed not to cause a crash. – Konrad Rudolph May 19 '22 at 13:48
  • 1
    @BonitaMontero it is, however I'm not a compiler implementer. My assumption is that this is carry over from prior implementation. E.g. it's consistent on [multiple architectures](https://godbolt.org/z/4Y84fvscP) whereas the value you suggest would need to be specified for _every single architecture_. – Mgetz May 19 '22 at 13:56
  • 1
    @Mgetz: Of course it would have to be defined for an implementation since the standard doesn't make any assumptions on that. – Bonita Montero May 19 '22 at 13:58
  • @KonradRudolph undefined behavior is undefined. That's the only response I have. – Mgetz May 19 '22 at 13:58
  • I don't think it is `-1`, that would limit things. It should be `~0` leaving `(1 << 64) - 1` valid offsets to be used as member pointers. Now fighting about `1 << 64` or `1 << 63` seems silly. But consider a 16 bit system and `struct A { char buf[32768]; int used;}`. Having `1 << 16` or `1 << 15` would make a real difference. Also most CPUs can represent an immediate of `~0` directly in opcodes but `1 << 63` would need to load a constant, possibly via GOT and indirections. – Goswin von Brederlow May 19 '22 at 13:59
  • @BonitaMontero yes but if I'm writing common tools or working with devs having conventions are a good thing. So having the convention always be -1 simplifies what has to be documented for a new architecture. But again... ask the GCC/MSVC devs. – Mgetz May 19 '22 at 13:59
  • 3
    @GoswinvonBrederlow -1 and ~0 are the same number – user253751 May 19 '22 at 14:07
  • @GoswinvonBrederlow: Didn't you notice my suggestion of (size_t)1 << 63 as a debugging aid ? – Bonita Montero May 19 '22 at 14:18
  • @user253751 `~(0LLU)` – Goswin von Brederlow May 19 '22 at 14:18
  • @BonitaMontero I saw. It would pretty reliably crash your code when you use it. But it's UB and you aren't supposed to use it at all. So compilers do not optimize for that case. They optimize for well formed code. And `~(0LLU)` is faster than `1LLU << 63` on many CPUs. – Goswin von Brederlow May 19 '22 at 14:21
  • 1
    It doesn't matter that's it UB because it's for debugging purposes only. – Bonita Montero May 19 '22 at 15:12
  • @GoswinvonBrederlow The whole point of the question is that *accidental use* of a nullptr member should ideally cause a crash, instead of silently doing something incorrect. For other cases of UB, compiler vendors are often careful to implement it such that accidental misuse triggers loud error messages (especially in debug mode). OP’s question is: why not also in this case, since it appears straightforward? – Konrad Rudolph May 19 '22 at 15:20
  • @KonradRudolph No, the question was why `-1` (or more likey `~(0LLU)`) was used when other values would be better for detecting ill-formed code. And the answer is speed and it being the least likely proper value. – Goswin von Brederlow May 19 '22 at 16:18
  • 1
    There's no speed difference in loading 0, -1 or (size_t)1 << 63. – Bonita Montero May 19 '22 at 17:01
  • @GoswinvonBrederlow The first sentence of your comment paraphrases my own comment while omitting crucial details, nothing more. The second sentence of your comment is completely wrong (*both* assertions in that sentence are incorrect). – Konrad Rudolph May 19 '22 at 17:10
  • 1
    @BonitaMontero, KonradRudolph: On x86_64 loading a 0 is `31 ff xor %edi,%edi`, which is the fastest. Loading a `~(0LLU)` is `48 c7 c7 ff ff ff ff mov $0xffffffffffffffff,%rdi`. Loading a `1LLU << 63` is `48 bf 00 00 00 00 00 00 00 80 movabs $0x8000000000000000,%rdi`. That's an extra 3 byte in the opcode which has some performance cost. On Mips64 the last needs a whole extra opcode: https://godbolt.org/z/3nehjcoM6 – Goswin von Brederlow May 19 '22 at 17:51
  • @GoswinvonBrederlow: As a member-pointer isn't casteable to anything the compiler can have arbitrary values for nullptr. So the compiler could have different values for optimized and non-optimized code. – Bonita Montero May 20 '22 at 04:32
  • @GoswinvonBrederlow Ah, I was wrong. Thanks for your last comment, it is illuminating. Mind converting it into an answer? I think this is probably (at least part of) the actual reason. – Konrad Rudolph May 20 '22 at 08:34

1 Answers1

4

There are three reasons why ~(0LLU) is preferable:

  1. Member pointers can be anything from 0 to the size of the struct or class. Using ~(0LLU) has the least risk of colliding with an actually valid member pointer. You can't really have a struct the size of size_t:

    <source>:2:21: error: size '9223372036854775808' of array 'x' exceeds maximum object size '9223372036854775807'
        2 |     long long x[1LLU<<63];
    
    <source>:2:15: error: size of array 'x' exceeds maximum object size '9223372036854775807'
        2 |     long long x[1LLU<<62];
    

    Note that limit is (1LLU<<63) - 1. So that kind of negates this argument. Might be different on a 16bit system.

  2. On x86_64 loading a 0, ~(1LLU) and 1LLU << 63 becomes

    31 ff                            xor    %edi,%edi
    48 c7 c7 ff ff ff ff             mov    $0xffffffffffffffff,%rdi
    48 bf 00 00 00 00 00 00 00 80    movabs $0x8000000000000000,%rdi
    

    Loading 0 is the fastest. Loading 1LLU << 63 is the longest opcode and that alone has performance costs. So using ~(0LLU) as the member pointer nullptr has a slight performance advantage.

    It's similar on many architectures. On Mips64 the last needs a whole extra opcode: https://godbolt.org/z/3nehjcoM6

  3. It's customary from the old C days that a function returns -1 or ~(0LLU) as error code except for pointers where 0 is used. Member pointers can't use 0.

Personally I think the compiler developers where just following old habits (reason 3). That it's also faster is just luck (or those old C geezers knew what they where doing choosing their error codes :).

As for why the compiler can't use ~(0LLU) when optimizing and 1LLU << 63 when debugging: You can compile some translation units as optimized code and some a debug code. They would then follow incompatible ABIs and couldn't be linked together.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42
  • You can't cast a member-pointer to anything and the value could be different in non-optimized and optimized code. – Bonita Montero May 23 '22 at 05:06
  • 1
    @BonitaMontero Nope. You can include the same header in to TUs and declare member pointers, pass them around, compare them to nullptr. They have to be compatible. – Goswin von Brederlow May 23 '22 at 08:37