Сравнение членов
Я читал о встроенных операторах сравнения. Мне стало интересно, почему нет операторов упорядочения (<
, <=
, >
, >=
) для указателей членов. Допустимо сравнивать адреса двух членов экземпляра структуры.
http://en.cppreference.com/w/cpp/language/operator_comparison:
3) Если в объекте типа класса, не являющегося объединением, два указателя указывают на разные нестатические элементы данных с одинаковым доступом к элементу или на подобъекты или элементы массива таких членов, рекурсивно, указатель на объявленный позже элемент сравнивается больше. Другими словами, члены класса в каждом из трех режимов доступа к элементам располагаются в памяти в порядке объявления.
С использованием адреса оператора (&
) и оператор разыменования указателя на член (.*
) можно сравнивать адреса, но нужен экземпляр.
Мои вопросы:
Почему нет встроенных операторов упорядочения для указателей членов?
Как сравнить два членских указателя без экземпляра?
Мой подход:
#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 ответа
Указатели участников - более сложные звери, чем вы думаете. Они состоят из указателя на потенциально существующий 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
Я не знаю, насколько это соответствует стандартам, но согласно 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
делать что хочешь.
РЕДАКТИРОВАТЬ
Как указывалось выше 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, очевидно, является наиболее разумным.