c++c++17language-lawyerstatic-membersnullptr

null pointer dereference when used as an lvalue


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.


Solution

  • 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.