2

我阅读了有关内置比较运算符的信息。我想知道为什么成员指针没有排序运算符(<、、、、<=)。比较结构实例化的两个成员的地址是有效的。>>=

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

3) 如果在一个非联合类类型的对象中,两个指针指向具有相同成员访问权限的不同非静态数据成员,或者指向这些成员的子对象或数组元素,递归地,指向后面声明的成员的指针进行比较更大。换句话说,三种成员访问模式中的每一种中的类成员都按声明顺序定位在内存中。

使用 adressof operator( &) 和成员指针解引用 operator( .*) 可以比较地址,但需要一个实例。

我的问题:

  1. 为什么没有用于成员指针的内置排序运算符?

  2. 如何在没有实例的情况下比较两个成员指针?

我的做法:

#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
};

我考虑了offsetof-macro,但它不将成员指针作为参数。

4

2 回答 2

2

成员指针比你想象的更复杂。它们由一个潜在存在的 vtable 的索引和一个偏移量组成(MSVC 在这方面被破坏了,但没有指定额外的选项)。

那是由于虚拟继承的存在,这意味着虚拟基础子对象的确切偏移量取决于最派生的类型,而不是用于访问的静态类型。
甚至虚拟基地的顺序也取决于此。

因此,您可以为指向同一虚拟基的元素或指向任何虚拟基之外的元素的成员指针创建一个总顺序。任何特定的实现甚至可能要求更多(接受强制的低效率),但这超出了标准的范围。

最后,在不了解实现细节和额外保证的情况下,您甚至不能依赖总订单。

大肠杆菌的例子

#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{});
}

示例输出:

offset of m.x: 8
offset of m.x: 12
于 2018-05-08T17:01:15.043 回答
0

我不知道这是如何符合标准的,但是根据Godbolt,以下代码可以在 clang、gcc 和 MSVC 中干净地编译,并以有效的方式生成正确的代码(基本上,对于 m2,push 4):

#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);
}

魔杖盒的输出:

Start
m1_offset=0, m2_offset=4
0
Finish

现在您可以使用或比较member_offset's来做任何您想做的事情。

编辑

正如上面 Caleth 和 Deduplicator 所指出的,这不适用于虚拟继承。有关原因,请参阅我对 Deduplicator 答案的最后评论。顺便说一句,我很感兴趣的是,在使用虚拟继承时访问基类中的实例变量时存在显着的运行时开销。我没有意识到这一点。

此外,以下简单的宏更易于使用,并且可以与 clang 的多重继承一起正常工作(对于所有那些花哨的模板来说就是如此):

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

您可以在Wandbox使用 gcc 和 clang 进行测试:

#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);
}

输出:

m1_offset=0, m2=4, m3=8

但是,如果将此宏与使用虚拟继承的类一起使用,则会得到 SEGFAULT(因为编译器需要查看对象内部以找到该对象数据成员的偏移量,并且那里没有对象 - 只是一个 nullptr)。

因此,OP 问题的答案是,在一般情况下,您确实需要一个实例来执行此操作。也许有一个特殊的构造函数,它不会以最小的开销创建其中一个。

第二次编辑

我想了更多,我想到了,而不是说:

int mo = member_offset (c, m);

你应该说:

constexpr int mo = member_offset (c, m);

如果类 c 正在使用虚拟继承,编译器会提醒您。

不幸的是,clang 和 gcc 都不会为任何类型的类编译这个,不管是否是虚拟继承。另一方面,如果类 c 使用虚拟继承,MSVC 会生成编译器错误,并且只会生成编译器错误。

我不知道哪个编译器在这里做正确的事情,就标准而言,但 MSVC 的行为显然是最明智的。

于 2018-05-09T08:52:06.360 回答