2

I read about the builtin comparison operators. I wondered why there are no ordering operators(<, <=, >, >=) for member pointers. It is valid to compare the adresses of two members of an instantiation of a struct.

http://en.cppreference.com/w/cpp/language/operator_comparison:

3) If, within an object of non-union class type, two pointers point to different non-static data members with the same member access, or to subobjects or array elements of such members, recursively, the pointer to the later declared member compares greater. In other words, class members in each of the three member access modes are positioned in memory in order of declaration.

With the use of the adressof operator(&) and the member pointer dereference operator(.*) it is possible to compare the adresses, but an instance is needed.

My questions:

  1. Why are there no builtin ordering operators for memberpointers?

  2. How to compare two memberpointers without an instance?

My approach:

#include <iostream>

    template<class S, class T>
int cmp_memberptr(T S::* a, T S::* b) {
    //S s; // works, but needed instanciation
    //S& s = std::declval<S>(); // error
    S& s = *(S*)nullptr; // no instanciation, works (on my machine), but undefined behavior because of nullptr dereference (most compilers warn directly)!

    // note: the precedence of .*:
    return int(&(s.*a) < &(s.*b)) - int(&(s.*a) > &(s.*b));
};

struct Point { int x, y; };

int main(int argc, char const* const* argv) {

    Point p;

    #define tst(t) std::cout << #t " is " << ((t) ? "true" : "false") << '\n'

    tst(&p.x < &p.y);
    //tst(&Point::x < &Point::y); // the main problem!
    tst(cmp_memberptr(&Point::x, &Point::y) < 0);

    #undef tst
};

I considered the offsetof-macro, but it does not take memberpointers as parameters.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
cmdLP
  • 1,658
  • 9
  • 19

2 Answers2

2

Member-pointers are more comlex beasts than you might think. They consist of an index into the potentially existing vtable and an offset (MSVC is broken in that regard without specifying extra options).

That is due to the existence of virtual inheritance, which means the exact offset of the virtual base sub-object depends on the most derived type, instead of the static type used for access.
Even the order of virtual bases depends on that.

So, you can create a total order for member-pointers pointing to elements of the same virtual base, or pointing to elements outside any virtual base. Any specific implementation might even mandate more (accepting the inefficiency that forces), but that's outside the purview of the standard.

In the end, you cannot rely on even having a total order without knowing implementation-details and having additional guarantees.

Example on coliru:

#include <iostream>

struct B {
    int x;
};
struct M : virtual B {};
struct D : M {
    int y;
};

static void print_offset(const M& m) {
    std::cout << "offset of m.x: " << ((char*)&m.x - (char*)&m) << '\n';
}

int main() {
    print_offset(M{});
    print_offset(D{});
}

Example output:

offset of m.x: 8
offset of m.x: 12
Deduplicator
  • 44,692
  • 7
  • 66
  • 118
0

I don't know how standards conformant this is, but according to Godbolt, the following code compiles cleanly in clang, gcc and MSVC and generates the right code (push 4, basically, for m2) in an efficient way:

#include "stdio.h"

template <typename T, typename M> int member_offset (M m)
{
    const void *pv = nullptr;
    const T *pT = static_cast <const T *> (pv);
    return static_cast <int> (reinterpret_cast <const char *> (&(pT->*m)) - reinterpret_cast <const char *> (pT));
}

class x
{
 public:
    int m1;
    int m2;
};

int main (void)
{
    int m1_offset = member_offset <x> (&x::m1);
    int m2_offset = member_offset <x> (&x::m2);
    printf ("m1_offset=%d, m2_offset=%d\n", m1_offset, m2_offset);
}

Output from Wandbox:

Start
m1_offset=0, m2_offset=4
0
Finish

Now you can just use or compare member_offset's to do whatever you want.

EDIT

As pointed out by Caleth and Deduplicator above, this doesn't work with virtual inheritance. See my last comment to Deduplicator's answer for the reason why. As an aside, it's interesting to me that there is significant runtime overhead when accessing instance variables in the base class when using virtual inheritance. I hadn't realised that.

Also, the following simple macro is easier to use and works correctly with multiple inheritance with clang (so much for all those fancy templates):

#define member_offset(classname, member) \
    ((int) ((char *) &((classname*) nullptr)->member - (char *) nullptr))

You can test this with gcc and clang at Wandbox:

#include "stdio.h"

#define member_offset(classname, member) \
        ((int) ((char *) &((classname *) nullptr)->member - (char *) nullptr))

struct a { int m1; };
struct b { int m2; };
struct c : a, b { int m3; };

int main (void)
{
    int m1_offset = member_offset (c, m1);
    int m2 = member_offset (c, m2);
    int m3 = member_offset (c, m3);
    printf ("m1_offset=%d, m2=%d, m3=%d\n", m1_offset, m2, m3);
}

Output:

m1_offset=0, m2=4, m3=8

But if you use this macro with a class that uses virtual inheritance, you get a SEGFAULT (because the compiler needs to look inside the object to find the offset of that object's data members and there is no object there - just a nullptr).

So, the answer to the OP's question is that you do need an instance to do this in the general case. Maybe have a special constructor that does nothing to create one of these with minimal overhead.

SECOND EDIT

I thought about this some more, and it occurred to me that instead of saying:

int mo = member_offset (c, m);

You should instead say:

constexpr int mo = member_offset (c, m);

Then the compiler should alert you if class c is using virtual inheritance.

Unfortunately, neither clang nor gcc will compile this for any kind of class, virtual inheritance or not. MSVC, on the other hand, does, and only generates a compiler error if class c uses virtual inheritance.

I don't know which compiler is doing the right thing here, insofar as the standard goes, but MSVC's behaviour is obviously the most sensible.

Paul Sanders
  • 24,133
  • 4
  • 26
  • 48