1

Background

I have a class containing different members (custom run time constructed structs). And I have a compile time tuple containing pairs of pointer-to-member elements and strings. Compile time I need to check if every pointer-to-member and name is used only once in the list, and the custom structs check if they have an entry in the tuple (they know their own pointer-to-member). Having a tuple for this purpose increases the compile time dramatically, it would be great to identify the members in compile time with a void* array and not with a heterogeneous data struct.

Attempt to solve problem

As I read in this thread, dereferencing a nullptr is not always undefined behavior.

I read CWG-issue #315 also, that states:

We agreed the example should be allowed. p->f() is rewritten as (*p).f() according to 5.2.5 [expr.ref]. *p is not an error when p is null unless the lvalue is converted to an rvalue (4.1 [conv.lval]), which it isn't here.

I wanted to leverage this to get a normal pointer from a pointer-to-member (I don't want to dereference them, I just want to compare pointers-to-members from the same class but with different types).

So I created the following code:

#include <iostream>

class Test
{
    int a;
public:
    static constexpr inline int Test::*memPtr = &Test::a;
    static constexpr inline int* intPtr = &(static_cast<Test*>(nullptr)->*Test::memPtr);
};
    
int main () {
    std::cout << Test::intPtr << std::endl;
}

In my opinion the &(static_cast<Test*>(nullptr)->*Test::memPtr); expression uses the same approach as the code that was discussed in CWG-issue #315.

The code above compiles with MSVC but not with clang or gcc.

I checked if similar code that was mentioned in #315 compiles or not:

struct Test {
  static constexpr int testFun () { return 10; } 
};

int main ()
{
  static constexpr int res{static_cast<Test*>(nullptr)->testFun()};
  static_assert(res == 10, "error");
}

And yes, it does. test code

Should the construct I used in the first example be available in constexpr expressions (as undefined behavior is not allowed there)?


Fun fact: If I modify my original code and add a virtual destructor to the class then both MSVC and clang are happy with it, and gcc crashes. I mean literally, it segfaults.

Fun fact 2: If I remove the virtual destructor and make the class templated gcc and MSVC compiles it, but now clang complains.

Marek R
  • 32,568
  • 6
  • 55
  • 140
Broothy
  • 659
  • 5
  • 20
  • The main difference is that in the CWG issue, `f` is a static member function. I believe you run afoul of [expr.mptr.oper.4](https://eel.is/c++draft/expr.mptr.oper#4): "If the result of E1 is an object whose type is not similar to the type of E1, or whose most derived object does not contain the member to which E2 refers, the behavior is undefined." Th result of E1 in your case is a dereferenced null pointer, which does not satisfy the requirements that it be similar to E1, nor that it have a member corresponding to E2. – Raymond Chen Feb 06 '23 at 11:10
  • @RaymondChen I see your point, the cited wording is quite straightforward. But then it contradicts CWG-issue #315's rational as well: `static_cast(nullptr)->testFun()`: here -> is the same pointer-to-member operator, but - according to the rational - it should work (and works in practice). – Broothy Feb 06 '23 at 11:57
  • 4
    `->` and `->*` are not the same operator. They follow different rules. In particular, `->*` imposes requirements on the runtime type of its left hand side. It also doesn't support static member functions. – Raymond Chen Feb 06 '23 at 12:07
  • Aaa I see, thank you for highlighting the difference! Now it makes sense :) I misinterpreted operator `->*` and thought that it is an operator `->` and a pointer-to-member dereference. But it is a special operator, I see now, thx. Then it is undefined behavior indeed, but the compilers seem to struggle identifying it. – Broothy Feb 06 '23 at 12:32
  • @Broothy : That's why it's UB rather than ill-formed. :-] – ildjarn Feb 06 '23 at 13:06
  • @ildjarn As far as I know expressions with undefined behavior should not be constant expressions: https://stackoverflow.com/questions/21319413/why-do-constant-expressions-have-an-exclusion-for-undefined-behavior – Broothy Feb 06 '23 at 13:20
  • *it should work (and works in practice)* Crashes on my machine. – Eljay Feb 06 '23 at 13:53
  • @Eljay it should work (and works in practice) - I mean the second example, this one: https://godbolt.org/z/vcvTfq6qr – Broothy Feb 06 '23 at 14:07
  • 2
    I think the misunderstanding is "*as undefined behavior is not allowed there*". Not all undefined behavior is detected at compile time during constexpr evaluation. The sentiment is basically correct, as UB is not allowed there, nor allowed anywhere else. But in many UB cases the UB goes undiagnosed by the compiler. (Personally, I consider this a dire shortcoming in C++. But it is what it is.) – Eljay Feb 06 '23 at 14:21
  • @ildjarn now I am confused what we are talking about. https://godbolt.org/z/vcvTfq6qr - This one should work and works in practice for all compilers, it is not undefined behavior. I referred this one when I wrote "it should work (and works in practice)". Then there is https://godbolt.org/z/1Wxf6PcMf - it works with MSVC and clang but crashes gcc and I accepted that it is UB because of the special ->* rules. I don't see any issue. As UB is not allowed in constant expressions MSVC and clang should not compile the second code and gcc should not crash. I think it is all. – Broothy Feb 06 '23 at 14:27
  • 2
    You said "*it is undefined behavior indeed, but the compilers seem to struggle identifying it*". Generally if the standard calls for UB rather than ill-formed in case of an 'error', it's because the committee had knowledge beforehand that implementation of correct diagnostics would be expensive and/or impossible. I was agreeing with your assessment that the compilers struggle with this, and noting that if they found it easy then it simply wouldn't have been UB in the first place; didn't mean to confuse things – ildjarn Feb 06 '23 at 14:40
  • @Broothy: "*get a normal pointer from a pointer-to-member*" Well, you can't. Pointers-to-member are never converted into a "normal pointer". There is no "normal pointer" equivalent to one as far as the C++ language is concerned. – Nicol Bolas Feb 06 '23 at 15:19
  • @NicolBolas I know that directly it is impossible. But with an object it is possible like: `const int* rawPtr = &(object.*memberPtr);`. The only issue is that I don't have an object to do it compile time, this is why I wanted to trick it with using `static_cast(nullptr)->*memberPtr` – Broothy Feb 06 '23 at 15:23
  • @Broothy: That returns a pointer to that subobject of `object`. There is nothing in `object`, so the pointer you get back is nonsense. If you just want to get a byte offset, use `offsetof`. – Nicol Bolas Feb 06 '23 at 15:24
  • @NicolBolas I know that it wouldn't point to any valid address. I just want to have the offset the given member has to be able to compare it with offsets of other members. With pointers-to-members it is not possible. I want this "pointer"/offset: https://imgur.com/a/Z3h6Bv8 – Broothy Feb 06 '23 at 15:30
  • 1
    `I just want to compare pointers-to-members from the same class but with different types)` WHY? What is actual goal? Also I not fully understated what "different types" means in that quote. Do you mean different fields of same class which do not have common type? Example do not show that. – Marek R Feb 06 '23 at 15:35
  • 1
    What's your end goal? Check byte offsets at compile-time? Then `offsetof`. Check byte offsets at runtime? Then `std::bit_cast(&A::b)` (or `memcpy` before C++20, with a `static_assert` on the member pointer `sizeof`), preferably with a small test to ensure that the internal representation of member-pointers is what we think it is. – HolyBlackCat Feb 06 '23 at 15:44
  • 1
    @MarekR I have a class containing different members (custom run time constructed structs). And I have a compile time tuple containing pairs of pointer-to-member elements and strings. Compile time I need to check if every pointer-to-member and name is used only once in the list, and the custom structs check if they have an entry in the tuple (they know their own pointer-to-member). Having a tuple for this purpose increases the compile time dramatically, it would be great to identify the members in compile time with a void* array and not with a heterogeneous data struct. – Broothy Feb 06 '23 at 15:56
  • @Broothy Cool this is vital information. Please [edit] your question and add it to top. Include code example with that. Even change title of question. This can be done using templates without hacking. You will get alternative solution to your problem. – Marek R Feb 06 '23 at 16:00
  • Alternatively you can create new question with this information. Note your current question has quite common problem with has own term: [XY problem](https://xyproblem.info/), so if you focus question on `X` you will gent proper solution. – Marek R Feb 06 '23 at 16:05
  • @MarekR I'm afraid it won't comply with SO rules. My original question is answered and I will accept the answer if Raymond Chen (he solved my question first) doesn't post his comment as an answer. I should open a new question. – Broothy Feb 06 '23 at 16:05
  • @Broothy: You cannot turn a pointer-to-member into an offset into a struct. "*And I have a compile time tuple containing pairs of pointer-to-member elements and strings.*" Maybe whoever put those values into that tuple of pairs should have used byte-offsets instead of pointer-to-members. – Nicol Bolas Feb 06 '23 at 16:35
  • I've copied important comment into a question, so other users do not have to scan all comments to get actual problem. But OP should provide more details if he wishes to get useful answear. – Marek R Feb 06 '23 at 17:02
  • @NicolBolas "Maybe whoever put those values into that tuple of pairs should have used byte-offsets instead of pointer-to-members." - I create the tuple and it seemed to be the only viable option. offsetof conditionally support non-standard layout types (and the types in my case can have virtual functions) so in my case it is a no-go. Do I have any other option to get the offset of a member? – Broothy Feb 07 '23 at 10:41

1 Answers1

3

From the standard on ->*'s behavior:

The expression E1->*E2 is converted into the equivalent form (*(E1)).*E2.

And for .*:

Abbreviating pm-expression.*cast-expression as E1.*E2, E1 is called the object expression. If the dynamic type of E1 does not contain the member to which E2 refers, the behavior is undefined.

The dynamic type of E1 (which dereferences a nullptr) does not exist, because it's a reference to no object. Therefore, the behavior of this expression is undefined.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982