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

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

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

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

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

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

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

Это влияет как на указатели на функции, так и на элементы данных

15

Решение

Липпмана «Внутри объектной модели C ++«есть обсуждение по этому поводу:

[там] есть необходимость сделать расположение виртуального базового класса внутри
каждый производный объект класса доступен во время выполнения. Например, в
следующий фрагмент программы:

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()Призывы. Скорее компилятор должен преобразовать код
делая доступ так, чтобы разрешение X::i может быть отложено до
во время выполнения.

По существу, наличие виртуального базового класса делает недействительной битовую копию
семантика
.

8

Другие решения

Короткий ответ:

Я считаю, что компилятор может сделать преобразование из 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,

Теперь рассмотрим пример ОП:

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 :: х ;? Дело не только в том, что вы не можете конвертировать p: у вас вообще не может быть указателя на член в виртуальной базе.

Причина в том, что выражение &B::x должен записывать только смещение x с начала B что неизвестно, как мы видели. Чтобы сделать эту работу, поняв, что B::x на самом деле является членом виртуальной базы A, компилятор должен будет создать что-то похожее на ptr_to_member<int, B> от &A::X который «помнит» A видел во время строительства и записывает смещение x с начала A,

2

По вопросам рекламы [email protected]