昨天,我和我的同事不确定为什么语言禁止这种转换
struct A { int x; };
struct B : virtual A { };
int A::*p = &A::x;
int B::*pb = p;
甚至演员都没有帮助。如果基本成员指针是虚拟基类,为什么标准不支持将基本成员指针转换为派生成员指针?
相关C ++标准参考:
类型为“cv
B
T
成员的类型”的prvalue,其中B
是类类型,可以转换为类型为“指向成员的指针”的prvalue属于D
类型的T
,其中D
是B
的派生类(第10条)。如果B
是不可访问的(第11条),不明确的(10.2)或虚拟的(10.1)基类D
,或虚基类的基类D
,需要进行此转换的程序格式不正确。
函数和数据成员指针都会受到影响。
答案 0 :(得分:8)
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
访问的物理偏移量pa
内的foo()
,因为pa
的实际类型可能会因每个类型而异foo()
的调用。相反,编译器必须转换代码 进行访问以便X::i
的分辨率可以延迟到 运行时。
基本上,虚拟基类的存在使按位复制无效 语义即可。
答案 1 :(得分: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
并未解决Steve Jessop提出的问题:
在与TemplateRex讨论之后,可以将这个问题简化为“我为什么不能做B :: * pb =&amp; B :: x;?这不仅仅是你不能转换p:你可以' t根本没有指向虚拟基础中成员的指针。
原因是表达式&B::x
应该只记录x
从B
开始的偏移量,这是我们所看到的未知的。为了使这项工作成功,在意识到B::x
实际上是虚拟基础A
的成员之后,编译器需要创建类似ptr_to_member<int, B>
的{{1}}类似于“{1}}的内容“&A::X
在施工时看到并记录A
从x
开头的偏移量。