![](/img/trans.png)
[英]How can I upcast a Python derived class to it's c++ base with Boost Python?
[英]C++ Will virtual functions still work if I upcast a derived object to its base type
考慮以下:
如果void *最初是B,它會調用A :: print還是B :: print?
#include <iostream> class A { public: static void w(void *p) { A *a = reinterpret_cast<A*>(p); a->print(); } virtual void print() { std::cout << "A" << std::endl; } }; class B : public A { public: void print() { std::cout << "B" << std::endl; } }; int main () { B b; A::w(&b); }
這為我打印B。
似乎已經轉換為A的void *仍然知道B被覆蓋的打印功能。 原因不是立即清楚。
有人可以向我解釋這是否是我可以依賴的行為,或者它是否只是一些有效的因素,因為它是一個小例子(比如返回對局部變量的引用並不總是在小程序中段錯誤)。
您的代碼具有未定義的行為
§5.2.10重新解釋演員
7將“指向T1的指針”類型的prvalue轉換為“指向T2的指針”類型(其中T1和T2是對象類型,T2的對齊要求不比T1更嚴格)並返回其原始類型會產生原始指針值。 未指定任何其他此類指針轉換的結果。
虛函數通常由隱式vtable
解析。 這基本上是類層次結構中每個虛函數的函數指針數組。 編譯器將其添加為您的類的“隱藏成員”。 調用虛函數時,調用vtable中的相應條目。
現在,當您創建類型B
的類時,它隱式地將B-vtable存儲在對象中。 強制轉換不會影響此表。
因此,當你將void *
為A
,會出現原始的vtable( B
類),它指向B::print
。
請注意,這是實現定義的行為,標准不保證這一點。 但大多數編譯器都會這樣做
首先,你的reinterpret_cast
是未定義的。 如果你將A*
傳遞給w
,它將被定義。
A * p = new B;
A::w(p);
delete p;
我建議使用static_cast<A*>(p)
,如果w
總是使用A*
調用。
如果你有一個定義的強制轉換為void*
並且返回內存地址保持不變。 所以, a
你的內部w
將是一個有效的A*
如果你通過一個有效的A*
至w
第一。
程序知道如何處理調用的問題與稱為“虛擬表”的機制有關。
注意:對於不同的編譯器,這可能有所不同。 我將談談Visual Studio如何處理它。
為簡單的繼承提供一些粗略的想法:
編譯器將在您的代碼中編譯2個print
函數: A::print
( 即地址X
)和B::print
( 即地址Y
)。
包含虛函數的類的實際內存占用量(即
struct A
{
void print (void);
size_t x;
};
struct B : A
{
void print (void);
size_t y;
};
)會有點像
struct Real_A
{
void * vtable;
size_t x;
};
struct Real_B : Real_A
{
size_t y;
};
此外,將有兩個所謂的虛擬表,每個類包含一個虛函數或一個具有虛函數的基類。
您可以將vtable視為包含每個函數的“真實”地址的結構。
在編譯時,編譯器將為每個類( A
和B
)創建Vtables:A的每個實例將具有vtable = <Address of A Vtable>
而B
每個實例將具有vtable = <Address of B Vtable>
。
在運行時,如果調用虛函數,程序將從Vtable中查找函數的“實際”地址,該地址存儲在作為A
或B
每個對象的第一個元素的地址處。
以下代碼是非標准的,並不是理智的 ...但它可能會給你一個想法...
#include <iostream>
struct A
{
virtual void print (void) { std::cout << "A called." << std::endl; }
size_t x;
};
struct B : A
{
void print (void) { std::cout << "B called." << std::endl; }
};
// "Real" memory layout of A
struct Real_A
{
void * vtable;
size_t x_value;
};
// "Real" memory layout of B
struct Real_B : Real_A
{
size_t y_value;
};
// "Pseudo virtual table structure for classes with 1 virtual function"
struct VT
{
void * func_addr;
};
int main (void)
{
A * pa = new A;
pa->x = 15;
B * pb = new B;
pb->x = 20;
A * pa_b = new B;
pa_b->x = 25;
// reinterpret addrress of A and B objects as Real_A and Real_B
Real_A& ra(*(Real_A*)pa);
Real_B& rb(*(Real_B*)pb);
// reinterpret addrress of B object through pointer to A as Real_B
Real_B& rb_a(*(Real_B*)pa_b);
// Print x_values to know whether we meet the class layout
std::cout << "Value of ra.x_value = " << ra.x_value << std::endl;
std::cout << "Value of rb.x_value = " << rb.x_value << std::endl;
std::cout << "Value of rb.x_value = " << rb_a.x_value << std::endl;
// Print vtable addresses
std::cout << "VT of A through A*: " << ra.vtable << std::endl;
std::cout << "VT of B through B*: " << rb.vtable << std::endl;
std::cout << "VT of B through A*: " << rb_a.vtable << std::endl;
// Reinterpret memory pointed to by the vtable address as VT objects
VT& va(*(VT*)ra.vtable);
VT& vb(*(VT*)rb.vtable);
VT& vb_a(*(VT*)rb_a.vtable);
// Print addresses of functions in the vtable
std::cout << "FA of A through A*: " << va.func_addr << std::endl;
std::cout << "FA of B through B*: " << vb.func_addr << std::endl;
std::cout << "FA of B through A*: " << vb_a.func_addr << std::endl;
delete pa;
delete pb;
delete pa_b;
return 0;
}
Visual Studio 2013輸出:
Value of ra.x_value = 15 Value of rb.x_value = 20 Value of rb.x_value = 25 VT of A through A*: 00D9DC80 VT of B through B*: 00D9DCA0 VT of B through A*: 00D9DCA0 FA of A through A*: 00D914B0 FA of B through B*: 00D914AB FA of B through A*: 00D914AB
gcc-4.8.1輸出:
Value of ra.x_value = 15 Value of rb.x_value = 20 Value of rb.x_value = 25 VT of A through A*: 0x8048f38 VT of B through B*: 0x8048f48 VT of B through A*: 0x8048f48 FA of A through A*: 0x8048d40 FA of B through B*: 0x8048cc0 FA of B through A*: 0x8048cc0
注意:無論您是通過A*
還是B*
訪問B對象,您都會先找到相同的vtable地址,並且您也會在vtable中找到相同的地址。
如果考慮在C ++中如何實現多重繼承,則reinterpret_cast無法工作。
基本上,當在相關類型之間進行轉換時,具有多重繼承,轉換可能涉及添加偏移量。 因此,在不知道源和目標類型的情況下,編譯器無法發出正確的指令。
因此,對於至少此用例,重新解釋強制轉換是可撤消的,因此它們被定義為未定義。
這里的危險部分,即使你不進行多重繼承,現代編譯器也開始將這種“未定義”行為解釋為意味着它們可以優化事物,它包含塊等等。 這幾乎肯定是有效的C ++標准(未定義意味着什么,一切都很好),但對開發人員來說可能是一個驚喜,開發人員傾向於將“未定義”理解為“代碼輸出可能無法正常工作”。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.