Вчера я и мой коллега не были уверены, почему язык запрещает это преобразование
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
, программа, которая требует этого преобразования, плохо сформирована.
Это влияет как на указатели на функции, так и на элементы данных
Липпмана «Внутри объектной модели 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
может быть отложено до
во время выполнения.
По существу, наличие виртуального базового класса делает недействительной битовую копию
семантика.
Короткий ответ:
Я считаю, что компилятор может сделать преобразование из 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
,