施法派生**→基地**有错吗?还有什么选择?
Is casting Derived** → Base** wrong? What's the alternative?
上下文
我的目标是有一个包含并操作几个基类对象的基本容器类,然后是一个包含和操作几个派生类对象的派生容器类。根据这个答案的建议,我试图通过让每个指针都包含一个指针数组(Base**
和Derived**
)来实现这一点,并在初始化基本容器类时从Derived**
强制转换为Base**
。
然而,我遇到了一个问题——尽管编译得很好,但在操作所包含的对象时,我会出现segfault,或者会调用错误的方法。
问题
我将问题归结为以下最小情况:
#include <iostream>
class Base1 {
public:
virtual void doThing1() {std::cout << "Called Base1::doThing1" << std::endl;}
};
class Base2 {
public:
virtual void doThing2() {std::cout << "Called Base2::doThing2" << std::endl;}
};
// Whether this inherits "virtual public" or just "public" makes no difference.
class Derived : virtual public Base1, virtual public Base2 {};
int main() {
Derived derived;
Derived* derivedPtrs[] = {&derived};
((Base2**) derivedPtrs)[0]->doThing2();
}
您可能希望打印"Called Base2::doThing2
",但是…
$ g++ -Wall -Werror main.cpp -o test && ./test
Called Base1::doThing1
实际上–代码调用Base2::doThing2
,但Base1::doThing1
最终被调用。我在更复杂的类中也遇到过这种segfault,所以我认为它是与地址相关的hijink(可能与vtable相关——如果没有virtual
方法,错误似乎不会发生)。您可以在此处运行它,并在此处查看它编译到的程序集。
你可以在这里看到我的实际结构——它更复杂,但它将它与上下文联系起来,并解释了为什么我需要这样的东西。
为什么Derived**
→Derived*
时Base**
强制转换无法正常工作→Base*
确实,更重要的是,将派生对象数组作为基对象数组来处理的正确方法是什么(否则,另一种方法可以创建一个可以包含多个派生对象的容器类)?
恐怕我无法在上转换(((Base2*) derivedPtrs[0])->doThing2()
)之前进行索引,因为在完整的代码中,该数组是一个类成员,而且我不确定在容器类中使用包含对象的每个位置手动转换是否是个好主意(甚至不可能)。不过,如果是处理此问题的方法,请更正我。
(我认为在这种情况下没有什么不同,但我所处的环境中std::vector
不可用。)
编辑:解决方案
许多答案表明,单独投射每个指针是拥有一个可以包含派生对象的数组的唯一方法——事实似乎确实如此。不过,对于我的特定用例,我设法使用模板解决了问题!通过为容器类应该包含的内容提供类型参数,而不必包含派生对象的数组,可以在编译时将数组的类型设置为派生类型(例如BaseContainer<Derived> container(length, arrayOfDerivedPtrs);
)。
这是上面损坏的"实际结构"代码的一个版本,用模板修复。
有很多事情让这个代码变得非常糟糕,并导致了这个问题:
-
为什么我们首先要处理两种恒星类型?如果
std::vector
不存在,为什么不写自己的呢? -
不要使用C样式强制转换。您可以将指向完全不相关类型的指针相互转换,编译器不允许阻止您(巧合的是,这正是这里发生的事情)。请改用
static_cast
/dynamic_cast
。 -
为了便于记法,我们假设我们有
std::vector
。您正在尝试将std::vector<Derived*>
强制转换为std::vector<Base*>
。这些都是不相关的类型(Derived**
和Base**
也是如此),将一个类型强制转换为另一个类型在任何方面都是不合法的。 -
从/到派生的指针强制转换并不一定是微不足道的。如果您有
struct X : A, B {}
,那么指向B
碱基的指针将不同于指向A
碱基的指针†(并且在播放vtable的情况下,也可能不同于指向X
的指针)。它们必须是,因为(子)对象不能位于同一内存地址。当您强制转换指针时,编译器将调整指针值。当然,如果您(试图)强制转换指针数组,那么对于每个单独的指针,这种情况不会/不可能发生。
如果您有一个指向Derived
的指针数组,并且想要获得一个指向Base
的指针的数组,那么您必须手动强制转换每个指针。由于两个数组中的指针值通常不同,因此无法"重用"同一个数组。
†(除非满足空基优化的条件,否则情况并非如此)。
就像其他人说的那样,问题是你没有让编译器完成调整索引的工作,假设内存中的派生布局是这样的(标准不保证,只是一种可能的实现):
| vtable_Base1 | Base1 | vtable_Base2 | Base2 | vtable_Derived | Derived |
然后&derived
指向对象的起点,当您通常进行时
Base2* base = static_cast<Derived*>(&derived)
编译器知道CCD_ 31结构在CCD_
相反,如果直接强制转换指针数组,则编译器强制执行该类型,假设您的数组已经存储了指向Base2
的指针,但没有对其进行调整。
一个肮脏的黑客,可能在你的情况下工作或不工作,是有一个方法返回指针到自己,例如:
class Base2 {
public:
Base2* base2() { return this; }
}
这样你就可以做CCD_ 34了。
它不起作用的原因与此不起作用相同:
struct Base {};
struct Derived : Base { int i; };
int main() {
Derived d[6];
Derived* d2 = d;
Base** b = &d2; // ERROR!
}
你的c型演员阵容是一种糟糕的做法,因为它没有警告你这个错误。只是不要对你的代码那样做。你的c型演员阵容实际上是伪装的reinterpret_cast
,这在这种情况下是完全错误的。
但是,为什么不能将派生的数组转换为基数组呢?简单:它们有不同的布局。
您可以看到,当您对某个类型的数组进行迭代时,数组中的每个元素在内存中都是连续的。Derived
类的大小可以是24字节,Base
的大小是8:
Derived d[4];
------------------------------------------------------
| D1 | D2 | D3 | D4 |
------------------------------------------------------
Base b[4];
---------------------
| B1 | B2 | B3 | B4 |
---------------------
正如您所看到的,Derived[4]
和Base[4]
是不同的类型,具有不同的布局。
那你能做什么?
事实上有很多解决方案。最简单的方法是创建一个新的指向基指针数组,并将每个派生指针强制转换为基指针。无论如何,您必须调整每个对象的指针。
它看起来是这样的:
std::vector<Base*> bases;
bases.reserve(std::size(derived_arr))
std::transform(
std::begin(derived_arr), std::end(derived_arr),
std::back_inserter(bases),
[](Dervied* d) {
// You must use dynamic cast because the
// pointer offset in only known at runtime
// when using virtual inheritance
return dynamic_cast<Base*>(d);
}
);
内存中的另一个解决方案是创建自己类型的迭代器,该迭代器将在调用operator*
和operator->
时执行强制转换。这有点难做,但可以通过使迭代稍微慢一点来节省分配。
最重要的是,这可能是不相关的,但我建议不要使用虚拟继承。这不是一个好的做法,根据我的经验,造成的痛苦比任何事情都多。我建议使用适配器模式并包装非多态类型。
从Derived*
强制转换为Base*
时,编译器将调整该值。当你施放Derived**
到Base**
时,你就打败了它。
这是始终使用static_cast
的一个很好的理由。更改代码的示例错误:
test.cpp:19:5: error: static_cast from 'Derived **' to 'Base2 **' is not
allowed
static_cast<Base2**>(derivedPtrs)[0]->doThing2();
(Base2 **)
实际上是reinterpret_cast
(您可以通过尝试所有四种类型转换来确认这一点),并且该表达式会导致UB。事实上,您可以隐式地将指向派生的指针强制转换为指向基的指针,这并不意味着它们是相同的,例如int
和float
。在这里,你引用的对象的类型不是,在这种情况下,会导致UB。
以这种方式调用虚拟函数会导致被调用对象的最终覆盖者。如何实现这一点取决于编译器。
假设编译器使用"vtable",从Derived
的内存地址中找到Base2
的vtable(可能是向memory.assembly添加偏移量)并非易事。
基本上,您必须对每个指针执行动态强制转换,将它们存储在某个位置,或者在需要时进行动态强制转换。
- 讨论 - 创建矩阵时的数组与向量的向量 - 什么是最实用的选择
- 在PostgreSQL中根据它们的ID选择大量行的最快方法是什么?
- 当我选择大于 720 的矩阵大小时,程序退出并显示错误代码.可能是什么原因?
- 从长(且合理)稀疏向量中选择随机元素的最有效方法是什么?
- 在 C++14 中,这种 C++17 倍表达的好选择是什么?
- 我什么时候应该选择"shared_ptr"而不是"make_shared"?
- 谷神星求解器:残差函子使用的可变对象是否良好实践?还有什么其他选择
- 我的选择排序功能C 有什么问题
- 将超时值设置为套接字轮询/选择的最佳实践是什么?
- 有什么方法可以让用户选择要输入哪个变量
- 施法派生**→基地**有错吗?还有什么选择?
- 我想将指向 const 对象的指针放入包含非 const 指针的容器中.我有什么选择
- 在什么条件下,数据库在从 cpp 执行选择查询时不会关闭游标
- 将Objective-C应用程序移植到Windows.什么是最好的选择
- 为什么预处理宏是邪恶的,还有什么选择
- C++接口 - 我有什么选择
- 试图使C++(MFC)代码片段易于重用.有什么选择
- 我有什么选择来构建一个在XP及更高版本上运行但具有最新功能的WTL程序
- C++同时使用两个不兼容的库,有什么选择
- 函数指针-不能更改函数签名,我有什么选择