Даже литой помогает. Почему стандарт не поддерживает преобразование указателя базового элемента в производный указатель элемента, если указатель базового элемента является виртуальным базовым классом?
На них влияют как указатели на элементы данных, так и данные.
Ответ 1
Lippman " Внутри модели объекта С++" обсуждается следующее:
[есть] необходимо сделать размещение виртуального базового класса внутри каждый объект производного класса доступен во время выполнения. Например, в следующий фрагмент программы:
class X { public: int i; };
class A : public virtual X { public: int j; };
class B : public virtual X { public: double d; };
class C : public A, public B { public: int k; };
// cannot resolve location of pa->X::i at compile-time
void foo( const A* pa ) { pa->i = 1024; }
main() {
foo( new A );
foo( new C );
// ...
}
компилятор не может исправить физическое смещение X::i
, доступ к которому осуществляется через pa
в пределах foo()
, так как фактический тип pa
может варьироваться в зависимости от foo()
invocations. Скорее, компилятор должен преобразовать код делая доступ так, чтобы разрешение X::i
можно было отложить до тех пор, пока во время выполнения.
По существу, наличие виртуального базового класса недействительно побитовая копия
Семантика.
Ответ 2
Короткий ответ:
Я считаю, что компилятор мог сделать преобразование от Base::*
до Derived::*
возможным, даже если Derived
получается практически из Base
. Для этого для работы указатель на элемент нужно записать больше, чем просто смещение. Также потребуется записать тип исходного указателя через какой-либо механизм стирания типа.
Таким образом, мое предположение состоит в том, что комитет думал, что это будет слишком много для функции, которая редко используется. Кроме того, что-то подобное может быть достигнуто с использованием чистой библиотеки. (См. Длинный ответ.)
Длинный ответ:
Надеюсь, мой аргумент не будет испорчен в каком-то угловом случае, но здесь мы идем.
По существу указатель на элемент записывает смещение члена относительно начала класса. Рассмотрим:
struct A { int x; };
struct B : virtual A { int y; };
struct C : B { int z; };
void print_offset(const B& obj) {
std::cout << (char*) &obj.x - (char*) &obj << '\n';
}
print_offset(B{});
print_offset(C{});
На моей платформе вывод 12
и 16
. Это показывает, что смещение a
по отношению к адресу obj
зависит от динамического типа obj
: 12
, если динамический тип B
и 16
, если он C
.
Теперь рассмотрим пример OP:
int A::*p = &A::x;
int B::*pb = p;
Как мы видели, для объекта статического типа B
смещение зависит от его динамического типа, а в двух строках выше не используется объект типа B
, поэтому нет никакого динамического типа для получения смещения.
Однако для разыменования указателя на элемент требуется объект. Не удалось компилятору взять объект, используемый в то время, чтобы получить правильное смещение? Или, другими словами, можно было бы отложить вычисление смещения до тех пор, пока мы не оценим obj.*pb
(где obj
имеет статический тип B
)?
Мне кажется, что это возможно. Этого достаточно, чтобы передать obj
в A&
и использовать смещение, записанное в pb
(которое он читает из p
), чтобы получить ссылку на obj.x
. Для этого pb
должен "помнить", что он был инициализирован из int A::*
.
Вот проект класса шаблона ptr_to_member
, который реализует эту стратегию. Специализация ptr_to_member<T, U>
должна работать аналогично T U::*
. (Обратите внимание, что это всего лишь черновик, который можно улучшить по-разному.)
template <typename Member, typename Object>
class ptr_to_member {
Member Object::* p_;
Member& (ptr_to_member::*dereference_)(Object&) const;
template <typename Base>
Member& do_dereference(Object& obj) const {
auto& base = static_cast<Base&>(obj);
auto p = reinterpret_cast<Member Base::*>(p_);
return base.*p;
}
public:
ptr_to_member(Member Object::*p) :
p_(p),
dereference_(&ptr_to_member::do_dereference<Object>) {
}
template <typename M, typename O>
friend class ptr_to_member;
template <typename Base>
ptr_to_member(const ptr_to_member<Member, Base>& p) :
p_(reinterpret_cast<Member Object::*>(p.p_)),
dereference_(&ptr_to_member::do_dereference<Base>) {
}
// Unfortunately, we can't overload operator .* so we provide this method...
Member& dereference(Object& obj) const {
return (this->*dereference_)(obj);
}
// ...and this one
const Member& dereference(const Object& obj) const {
return dereference(const_cast<Object&>(obj));
}
};
Вот как это следует использовать:
A a;
ptr_to_member<int, A> pa = &A::x; // int A::* pa = &::x
pa.dereference(a) = 42; // a.*pa = 42;
assert(a.x == 42);
B b;
ptr_to_member<int, B> pb = pa; // int B::* pb = pa;
pb.dereference(b) = 43; // b*.pb = 43;
assert(b.x == 43);
C c;
ptr_to_member<int, B> pc = pa; // int B::* pc = pa;
pc.dereference(c) = 44; // c.*pd = 44;
assert(c.x == 44);
К сожалению, только ptr_to_member
не решает проблему, поднятую Стив Джессопом:
После обсуждения с TemplateRex можно ли упростить этот вопрос: "Почему я не могу сделать int B:: * pb = & B:: x;? Это не просто то, что вы не можете преобразовать p: t имеет указатель-член для члена в виртуальной базе вообще.
Причина в том, что выражение &B::x
должно записывать только смещение x
с начала B
, которое не указано, как мы видели. Чтобы сделать эту работу, осознав, что B::x
фактически является членом виртуальной базы a
, компилятору необходимо создать нечто похожее на ptr_to_member<int, B>
из &A::X
, которое "запоминает" a
, видимое при построении время и записывает смещение x
с начала a
.