Сравнение членов - PullRequest
       34

Сравнение членов

0 голосов
/ 08 мая 2018

Я читал о встроенных операторах сравнения. Мне было интересно, почему нет указателей (<, <=, >, >=) для указателей членов. Допустимо сравнивать адреса двух членов экземпляра структуры.

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

3) Если внутри объекта типа класса, не являющегося объединением, два указателя указывают на разные нестатические члены-данные с одинаковым доступом к элементу или на подобъекты или элементы массива таких членов, рекурсивно, указатель на последующий заявленный участник сравнивает больше. Другими словами, члены класса в каждом из трех режимов доступа к элементам располагаются в памяти в порядке объявления.

С использованием оператора адреса (&) и оператора разыменования указателя элемента (.*) можно сравнивать адреса, но необходим экземпляр.

Мои вопросы:

  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, но он не принимает указатели-члены в качестве параметров.

Ответы [ 2 ]

0 голосов
/ 09 мая 2018

Я не знаю, насколько это соответствует стандартам, но согласно Godbolt , следующий код компилируется чисто в clang, gcc и MSVC и генерирует правильный код (push 4, в основном, для m2) эффективным способом:

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

Вывод из Wandbox :

Start
m1_offset=0, m2_offset=4
0
Finish

Теперь вы можете просто использовать или сравнить member_offset's, чтобы делать все, что вы хотите.

EDIT

Как указывалось выше Caleth и Deduplicator, это не работает с виртуальным наследованием. Смотрите мой последний комментарий к ответу Дедупликатора по причине. Кроме того, мне интересно, что при доступе к переменным экземпляра в базовом классе при использовании виртуального наследования возникают значительные накладные расходы. Я этого не понял.

Кроме того, следующий простой макрос проще в использовании и корректно работает с множественным наследованием с помощью clang (так много для всех этих причудливых шаблонов):

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

Вы можете проверить это с помощью gcc и clang в 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);
}

Выход:

m1_offset=0, m2=4, m3=8

Но если вы используете этот макрос с классом, который использует виртуальное наследование, вы получите SEGFAULT (потому что компилятору нужно заглянуть внутрь объекта, чтобы найти смещение членов данных этого объекта, и там нет объекта - просто nullptr ).

Итак, ответ на вопрос ОП заключается в том, что вам нужен экземпляр для этого в общем случае. Возможно, есть специальный конструктор, который ничего не делает для создания одного из них с минимальными издержками.

ВТОРОЕ РЕДАКТИРОВАНИЕ

Я подумал еще об этом, и мне пришло в голову, что вместо того, чтобы сказать:

int mo = member_offset (c, m);

Вместо этого вы должны сказать:

constexpr int mo = member_offset (c, m);

Тогда компилятор должен предупредить вас, если класс c использует виртуальное наследование.

К сожалению, ни clang, ни gcc не скомпилируют это для какого-либо класса, виртуального наследования или нет. MSVC, с другой стороны, делает и генерирует ошибку компилятора, только если класс c использует виртуальное наследование.

Я не знаю, какой компилятор делает все правильно, поскольку стандарт идет, но поведение MSVC, очевидно, является наиболее разумным.

0 голосов
/ 08 мая 2018

Члены-указатели - более сложные звери, чем вы думаете. Они состоят из индекса потенциально существующей таблицы и смещения (в этом отношении 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
...