Почему запрещается конвертировать из VirtualBase:: * в Derived:: *?

Вчера я и мой коллега не были уверены, почему язык запрещает это преобразование

struct A { int x; };
struct B : virtual A { };

int A::*p = &A::x;
int B::*pb = p;

Даже литой помогает. Почему стандарт не поддерживает преобразование указателя базового элемента в производный указатель элемента, если указатель базового элемента является виртуальным базовым классом?

Соответствующая стандартная ссылка на С++:

Значение praleue типа "указатель на член B типа cv T", где B - тип класса, может быть преобразован в prvalue типа "указатель на член D типа cv T", где D - производный класс (раздел 10) B. Если B является недоступным (клаузулом 11), двусмысленным (10.2) или виртуальным (10.1) базовым классом D или базовым классом виртуального базового класса D, программа, требующая этого преобразования, плохо сформирован.

На них влияют как указатели на элементы данных, так и данные.

Ответы

Ответ 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.