为什么不允许从 VirtualBase::* 转换为 Derived::*?

Why is it disallowed to convert from VirtualBase::* to Derived::*?

本文关键字:Derived 转换 不允许 VirtualBase 为什么      更新时间:2023-10-16

昨天,我和我的同事不确定为什么语言禁止这种转换

struct A { int x; };
struct B : virtual A { };
int A::*p = &A::x;
int B::*pb = p;

甚至连演员都无济于事。如果基成员指针是虚拟基类,为什么标准不支持将基成员指针转换为派生成员指针?

相关C++标准参考:

类型为"指向 cvT类型B成员的指针"的 prvalue,其中B是类类型,可以转换为类型为 "指向 cvTD的成员的指针"的 prvalue 值,其中DB的派生类(条款 10)。如果B是不可访问的(条款 11)、不明确的 (10.2) 或虚拟的 (10.1)D基类,或虚拟基类的基类D,则需要此转换的程序格式不正确。

函数和数据成员指针都会受到影响。

Lippman的">Inside the C++ Object model"对此进行了讨论:

[那里]需要使虚拟基类位于 每个派生类对象在运行时可用。例如,在 以下程序片段:

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的物理偏移量pafoo()内,因为实际的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{});

在我的平台上,输出是1216。这表明a相对于obj地址的偏移量取决于obj的动态类型:12动态类型是否B,如果动态类型C,则16

现在考虑 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:你根本不能在虚拟基地中有一个指向成员的指针。

原因是表达式&B::x应该只记录xB开头的偏移量,正如我们所看到的,这是未被理解的。为了完成这项工作,在意识到B::x实际上是虚拟基A的成员后,编译器需要从&A::X创建类似于ptr_to_member<int, B>的东西,以"记住"在构造时看到的A并记录xA开始的偏移量。