อะไรคือการใช้งานที่เหมาะสมของ:
static_cast
dynamic_cast
const_cast
reinterpret_cast
- หล่อสไตล์ C
(type)value
- ฟังก์ชั่นหล่อสไตล์
type(value)
เราจะตัดสินใจได้อย่างไรว่าจะใช้ในกรณีใด
อะไรคือการใช้งานที่เหมาะสมของ:
static_cast
dynamic_cast
const_cast
reinterpret_cast
(type)value
type(value)
เราจะตัดสินใจได้อย่างไรว่าจะใช้ในกรณีใด
คำตอบ:
static_cast
เป็นนักแสดงคนแรกที่คุณควรพยายามใช้ มันทำสิ่งต่าง ๆ เช่นการแปลงโดยนัยระหว่างประเภท (เช่นint
ถึงfloat
หรือตัวชี้ไปยังvoid*
) และยังสามารถเรียกใช้ฟังก์ชันการแปลงที่ชัดเจน (หรือที่อยู่โดยนัย) ในหลายกรณีการระบุอย่างชัดเจนstatic_cast
ไม่จำเป็น แต่เป็นสิ่งสำคัญที่จะต้องทราบว่าT(something)
ไวยากรณ์นั้นเทียบเท่า(T)something
และควรหลีกเลี่ยง (เพิ่มเติมในภายหลัง) อย่างไรก็ตามT(something, something_else)
มีความปลอดภัยและรับประกันว่าจะเรียกตัวสร้าง
static_cast
ยังสามารถส่งผ่านลำดับชั้นการสืบทอด ไม่จำเป็นเมื่อทำการร่ายขึ้นไป (ไปยังคลาสเบส) แต่เมื่อทำการร่ายลงมันสามารถใช้งานได้ตราบใดที่มันไม่ผ่านการvirtual
ถ่ายทอดทางพันธุกรรม มันไม่ได้ทำการตรวจสอบและมันเป็นพฤติกรรมที่ไม่ได้กำหนดเพื่อstatic_cast
ลดลำดับชั้นเป็นประเภทที่ไม่ใช่ประเภทของวัตถุ
const_cast
สามารถใช้เพื่อลบหรือเพิ่มconst
ตัวแปร นักแสดง C ++ รายอื่นไม่สามารถลบได้ (ไม่ใช่แม้แต่reinterpret_cast
) เป็นสิ่งสำคัญที่จะต้องทราบว่าการแก้ไขconst
ค่าเดิมนั้นไม่ได้กำหนดไว้หากตัวแปรดั้งเดิมคือconst
; หากคุณใช้เพื่อconst
ถอดการอ้างอิงถึงสิ่งที่ไม่ได้ประกาศconst
ไว้มันปลอดภัย สิ่งนี้มีประโยชน์เมื่อโอเวอร์โหลดฟังก์ชันของสมาชิกตามconst
เช่น นอกจากนี้ยังสามารถใช้เพื่อเพิ่มconst
วัตถุเช่นการเรียกฟังก์ชั่นสมาชิกเกินพิกัด
const_cast
ยังใช้งานได้เหมือนกันvolatile
แม้ว่าจะเป็นเรื่องธรรมดาน้อยกว่า
dynamic_cast
ใช้เฉพาะสำหรับการจัดการความแตกต่าง คุณสามารถใช้พอยน์เตอร์หรือการอ้างอิงถึงชนิด polymorphic ใด ๆ ไปยังประเภทคลาสอื่น ๆ (ประเภท polymorphic มีอย่างน้อยหนึ่งฟังก์ชั่นเสมือนจริงประกาศหรือสืบทอด) คุณสามารถใช้มันได้มากกว่าแค่การเหวี่ยงลง - คุณสามารถเหวี่ยงไปด้านข้างหรือแม้กระทั่งโซ่อื่น ๆ dynamic_cast
จะหาวัตถุที่ต้องการและส่งกลับมาถ้าเป็นไปได้ หากไม่สามารถทำได้จะส่งคืนnullptr
ในกรณีของตัวชี้หรือส่งstd::bad_cast
ในกรณีของการอ้างอิง
dynamic_cast
มีข้อ จำกัด บางอย่าง มันจะไม่ทำงานหากมีวัตถุหลายประเภทที่เหมือนกันในลำดับชั้นการสืบทอด (ที่เรียกว่า 'เพชรที่น่ากลัว') และคุณไม่ได้ใช้การvirtual
สืบทอด นอกจากนี้ยังสามารถผ่านการรับมรดกสาธารณะเท่านั้น - จะไม่สามารถเดินทางผ่านprotected
หรือprivate
รับมรดกได้เสมอ อย่างไรก็ตามนี่เป็นปัญหาที่ไม่ค่อยเกิดขึ้นเนื่องจากรูปแบบของการสืบทอดนั้นหายาก
reinterpret_cast
เป็นนักแสดงที่อันตรายที่สุดและควรใช้อย่างไม่ จำกัด เปลี่ยนประเภทหนึ่งเป็นประเภทอื่นโดยตรง - เช่นการคัดเลือกค่าจากตัวชี้หนึ่งไปยังอีกประเภทหนึ่งหรือเก็บตัวชี้ไว้ในint
หรือสิ่งแปลก ๆ อื่น ๆ ทุกประเภท ส่วนใหญ่การรับประกันเดียวที่คุณได้รับreinterpret_cast
คือโดยปกติถ้าคุณส่งผลลัพธ์กลับไปเป็นประเภทเดิมคุณจะได้รับค่าเดียวกันแน่นอน (แต่ไม่ใช่ถ้าประเภทกลางมีขนาดเล็กกว่าประเภทดั้งเดิม) มีจำนวนการแปลงที่reinterpret_cast
ไม่สามารถทำได้เช่นกัน ส่วนใหญ่จะใช้สำหรับการแปลงแปลก ๆ และการปรับเปลี่ยนบิตเช่นเปลี่ยนกระแสข้อมูลดิบเป็นข้อมูลจริงหรือเก็บข้อมูลในบิตต่ำของตัวชี้ไปยังข้อมูลที่จัดตำแหน่ง
นักแสดงสไตล์ Cและนักแสดงสไตล์ฟังก์ชั่นมีการใช้งาน(type)object
หรือtype(object)
ตามลำดับและเทียบเท่าฟังก์ชั่น พวกเขาถูกกำหนดให้เป็นคนแรกของต่อไปนี้ซึ่งประสบความสำเร็จ:
const_cast
static_cast
(แม้ว่าจะเพิกเฉยต่อข้อ จำกัด การเข้าถึง)static_cast
(ดูด้านบน) แล้ว const_cast
reinterpret_cast
reinterpret_cast
จากนั้น const_cast
ดังนั้นจึงสามารถใช้แทนการปลดเปลื้องอื่น ๆ ได้ในบางกรณี แต่อาจเป็นอันตรายอย่างยิ่งเนื่องจากความสามารถในการเปลี่ยนเป็น a reinterpret_cast
และควรเลือกใช้หลังเมื่อต้องการการร่ายอย่างชัดเจนเว้นแต่คุณแน่ใจว่าstatic_cast
จะสำเร็จหรือreinterpret_cast
ล้มเหลว . ถึงกระนั้นลองพิจารณาตัวเลือกที่ยาวและชัดเจนมากขึ้น
static_cast
cast -style casts ยังละเว้นการควบคุมการเข้าถึงเมื่อทำการ a ซึ่งหมายความว่าพวกมันมีความสามารถในการดำเนินการที่ไม่มี cast อื่น ๆ แม้ว่านี่จะเป็นกระบอง แต่ในใจของฉันก็เป็นอีกสาเหตุหนึ่งที่ทำให้หลีกเลี่ยงการแสดงใน C
const
ได้ (ไม่ถึงreinterpret_cast
)" ... จริงเหรอ เกี่ยวกับreinterpret_cast<int *>(reinterpret_cast<uintptr_t>(static_cast<int const *>(0)))
อะไร
reinterpret_cast
มักเป็นอาวุธที่เลือกใช้เมื่อจัดการกับชุดข้อมูลทึบแสงของ API
ใช้dynamic_cast
สำหรับการแปลงพอยน์เตอร์ / การอ้างอิงภายในลำดับชั้นการสืบทอด
ใช้static_cast
สำหรับการแปลงประเภทธรรมดา
ใช้reinterpret_cast
สำหรับการตีความรูปแบบบิตในระดับต่ำอีกครั้ง ใช้ด้วยความระมัดระวังอย่างยิ่ง
ใช้สำหรับการหล่อออกไปconst_cast
const/volatile
หลีกเลี่ยงสิ่งนี้หากคุณไม่ได้ใช้ API ที่ไม่ถูกต้อง
(มีการอธิบายทางทฤษฎีและแนวคิดมากมาย)
ด้านล่างนี้คือบางส่วนของตัวอย่างการปฏิบัติเมื่อใช้static_cast , dynamic_cast , const_cast , reinterpret_cast
(อ้างถึงสิ่งนี้เพื่อเข้าใจการอธิบาย: http://www.cplusplus.com/doc/tutorial/typecasting/ )
static_cast:
OnEventData(void* pData)
{
......
// pData is a void* pData,
// EventData is a structure e.g.
// typedef struct _EventData {
// std::string id;
// std:: string remote_id;
// } EventData;
// On Some Situation a void pointer *pData
// has been static_casted as
// EventData* pointer
EventData *evtdata = static_cast<EventData*>(pData);
.....
}
dynamic_cast:
void DebugLog::OnMessage(Message *msg)
{
static DebugMsgData *debug;
static XYZMsgData *xyz;
if(debug = dynamic_cast<DebugMsgData*>(msg->pdata)){
// debug message
}
else if(xyz = dynamic_cast<XYZMsgData*>(msg->pdata)){
// xyz message
}
else/* if( ... )*/{
// ...
}
}
const_cast:
// *Passwd declared as a const
const unsigned char *Passwd
// on some situation it require to remove its constness
const_cast<unsigned char*>(Passwd)
reinterpret_cast:
typedef unsigned short uint16;
// Read Bytes returns that 2 bytes got read.
bool ByteBuffer::ReadUInt16(uint16& val) {
return ReadBytes(reinterpret_cast<char*>(&val), 2);
}
static_cast<char*>(&val)
หรือไม่
static_cast
/ จาก void *
สำหรับทุกอย่างมีการปลดเปลื้องอื่น ๆ ประเภทreinterpret cast
ใดก็ได้ที่char *
ได้รับอนุญาตให้อ่านการเป็นตัวแทนของวัตถุใด ๆ - และหนึ่งในกรณีเดียวที่คำหลักนั้นมีประโยชน์ไม่ใช่เครื่องกำเนิดอาละวาดของพฤติกรรมการใช้งาน / ไม่ได้กำหนด แต่นี้จะไม่ถือว่าเป็น 'ปกติ' แปลงจึงไม่ได้รับอนุญาตตาม (ปกติ) static_cast
อนุรักษ์นิยมมาก
มันอาจช่วยได้ถ้าคุณรู้เรื่องภายในเล็กน้อย ...
static_cast
static_cast
สำหรับพวกเขาA
การB
, static_cast
โทรB
คอนสตรัค 's ผ่านA
เป็นพารามิเตอร์ หรือA
อาจมีตัวดำเนินการแปลง (เช่นA::operator B()
) หากB
ไม่มีคอนสตรัคเตอร์ดังกล่าวหรือA
ไม่มีโอเปอเรเตอร์การแปลงคุณจะได้รับข้อผิดพลาดในการรวบรวมเวลาA*
การB*
เสมอประสบความสำเร็จถ้า A และ B อยู่ในลำดับชั้นมรดก (หรือเป็นโมฆะ) มิฉะนั้นคุณจะได้รับการรวบรวมข้อผิดพลาดA&
B&
dynamic_cast
(Base*)
การ(Derived*)
อาจล้มเหลวหากตัวชี้ไม่จริงชนิดที่ได้มาA*
ถึงB*
หากการส่งไม่ถูกต้อง dynamic_cast จะส่งคืน nullptrA&
การB&
ถ้าหล่อไม่ถูกต้องแล้ว dynamic_cast จะโยนยกเว้น bad_castconst_cast
set<T>
ซึ่งส่งคืนองค์ประกอบของมันเป็น const เท่านั้นเพื่อให้แน่ใจว่าคุณจะไม่เปลี่ยนคีย์ อย่างไรก็ตามหากคุณต้องการแก้ไขสมาชิกที่ไม่ได้ใช้คีย์ของวัตถุคุณควรจะยอมรับ คุณสามารถใช้ const_cast เพื่อลบความขุ่นT& SomeClass::foo()
const T& SomeClass::foo() const
เพื่อหลีกเลี่ยงการทำสำเนารหัสคุณสามารถใช้ const_cast เพื่อคืนค่าของฟังก์ชันหนึ่งจากอีกฟังก์ชันหนึ่งreinterpret_cast
If you cast base pointer to derived pointer but if actual object is not really derived type then you don't get error. You get bad pointer and segfault at runtime.
คุณได้รับ UB ซึ่งอาจส่งผลให้ segfault ที่รันไทม์หากคุณโชคดี 2. Dynamic casts สามารถใช้ในการร่ายข้ามได้ 3. การปลดเปลื้อง Const อาจส่งผลให้ UB ในบางกรณี การใช้mutable
อาจเป็นตัวเลือกที่ดีกว่าในการใช้ความมั่นคงทางตรรกะ
mutable
การคัดเลือกข้าม ฯลฯ
ทำสิ่งนี้ตอบคำถามของคุณ?
ฉันไม่เคยใช้reinterpret_cast
และสงสัยว่าทำงานในกรณีที่ต้องการมันไม่ได้เป็นกลิ่นของการออกแบบที่ไม่ดี ในฐานรหัสฉันทำงานอยู่dynamic_cast
มีการใช้งานมาก ข้อแตกต่าง static_cast
คือการdynamic_cast
ตรวจสอบรันไทม์ซึ่งอาจ (ปลอดภัยกว่า) หรืออาจจะไม่ (เพิ่มมากขึ้น) เป็นสิ่งที่คุณต้องการ (ดูmsdn )
reinterpret_cast
เพื่อแยกข้อมูลออกจากอาร์เรย์ ตัวอย่างเช่นถ้าฉันมีchar*
บัฟเฟอร์ขนาดใหญ่ที่เต็มไปด้วยข้อมูลไบนารีที่อัดแน่นที่ฉันจำเป็นต้องย้ายผ่านและรับชนิดดั้งเดิมที่แตกต่างกันไป สิ่งนี้:template<class ValType> unsigned int readValFromAddress(char* addr, ValType& val) { /*On platforms other than x86(_64) this could do unaligned reads, which could be bad*/ val = (*(reinterpret_cast<ValType*>(addr))); return sizeof(ValType); }
reinterpret_cast
มันมีประโยชน์ไม่มากนัก
reinterpret_cast
ใช้เพียงเหตุผลเดียว ฉันเคยเห็นข้อมูลวัตถุดิบที่เก็บไว้ในประเภทข้อมูล "หยด" ในฐานข้อมูลจากนั้นเมื่อข้อมูลถูกดึงจากฐานข้อมูลreinterpret_cast
จะใช้ในการเปลี่ยนข้อมูลดิบนี้เป็นวัตถุ
นอกเหนือจากคำตอบอื่น ๆ จนถึงตอนนี้นี่คือตัวอย่างที่static_cast
ไม่ชัดเจนที่ไม่เพียงพอดังนั้นจึงreinterpret_cast
เป็นสิ่งจำเป็น สมมติว่ามีฟังก์ชั่นซึ่งในพารามิเตอร์ขาออกส่งกลับพอยน์เตอร์ไปยังวัตถุของคลาสที่แตกต่างกัน (ซึ่งไม่ได้แชร์คลาสพื้นฐานทั่วไป) ตัวอย่างจริงของฟังก์ชันดังกล่าวคือCoCreateInstance()
(ดูพารามิเตอร์สุดท้ายซึ่งอันที่จริงแล้วvoid**
) สมมติว่าคุณร้องขอคลาสของวัตถุเฉพาะจากฟังก์ชันนี้เพื่อให้คุณทราบล่วงหน้าถึงชนิดของตัวชี้ (ซึ่งคุณมักจะทำกับวัตถุ COM) ในกรณีนี้คุณไม่สามารถโยนตัวชี้ไปยังตัวชี้ของคุณเข้าvoid**
กับstatic_cast
: reinterpret_cast<void**>(&yourPointer)
คุณจำเป็นต้อง
ในรหัส:
#include <windows.h>
#include <netfw.h>
.....
INetFwPolicy2* pNetFwPolicy2 = nullptr;
HRESULT hr = CoCreateInstance(__uuidof(NetFwPolicy2), nullptr,
CLSCTX_INPROC_SERVER, __uuidof(INetFwPolicy2),
//static_cast<void**>(&pNetFwPolicy2) would give a compile error
reinterpret_cast<void**>(&pNetFwPolicy2) );
อย่างไรก็ตามstatic_cast
ทำงานได้กับพอยน์เตอร์ง่าย ๆ (ไม่ใช่พอยน์เตอร์ถึงพอยน์เตอร์) ดังนั้นโค้ดข้างต้นสามารถเขียนใหม่เพื่อหลีกเลี่ยงreinterpret_cast
(ในราคาของตัวแปรพิเศษ) ด้วยวิธีต่อไปนี้:
#include <windows.h>
#include <netfw.h>
.....
INetFwPolicy2* pNetFwPolicy2 = nullptr;
void* tmp = nullptr;
HRESULT hr = CoCreateInstance(__uuidof(NetFwPolicy2), nullptr,
CLSCTX_INPROC_SERVER, __uuidof(INetFwPolicy2),
&tmp );
pNetFwPolicy2 = static_cast<INetFwPolicy2*>(tmp);
&static_cast<void*>(pNetFwPolicy2)
แทนที่จะstatic_cast<void**>(&pNetFwPolicy2)
?
ในขณะที่คำตอบอื่น ๆ ที่อธิบายไว้อย่างแตกต่างระหว่าง C ++ ปลดเปลื้องผมอยากจะเพิ่มบันทึกสั้น ๆ ว่าทำไมคุณไม่ควรใช้บรรยากาศแบบ C และ(Type) var
Type(var)
สำหรับผู้เริ่มต้น C + + C-style casts ดูเหมือนว่าเป็นการดำเนินการ superset บน C ++ casts (static_cast <> (), dynamic_cast <> (), const_cast <> (), reinterpret_cast <> ()) และบางคนอาจชอบพวกมันมากกว่า C ++ casts . อันที่จริงแล้วการหล่อแบบ C นั้นเป็นแบบซูเปอร์เซ็ตและสั้นกว่าในการเขียน
ปัญหาหลักของ C-style casts คือพวกเขาซ่อนเจตนาที่แท้จริงของนักพัฒนา cast-style cast สามารถทำการร่ายได้ทุกประเภทจากการร่ายความปลอดภัยตามปกติโดย static_cast <> () และ dynamic_cast <> () ถึงการร่ายที่อาจเป็นอันตรายเช่น const_cast <> () ซึ่งตัวดัดแปลง const สามารถลบได้ดังนั้นตัวแปร const สามารถแก้ไขและ reinterpret_cast <> () ที่สามารถตีความค่าจำนวนเต็มเป็นตัวชี้ได้อีกครั้ง
นี่คือตัวอย่าง
int a=rand(); // Random number.
int* pa1=reinterpret_cast<int*>(a); // OK. Here developer clearly expressed he wanted to do this potentially dangerous operation.
int* pa2=static_cast<int*>(a); // Compiler error.
int* pa3=dynamic_cast<int*>(a); // Compiler error.
int* pa4=(int*) a; // OK. C-style cast can do such cast. The question is if it was intentional or developer just did some typo.
*pa4=5; // Program crashes.
เหตุผลหลักที่เพิ่มการใช้งาน C ++ ในภาษานั้นเพื่อให้นักพัฒนาสามารถอธิบายความตั้งใจของเขาได้ - ทำไมเขาถึงต้องลงมือทำ ด้วยการใช้งาน C-style casts ที่ใช้ได้อย่างสมบูรณ์ใน C ++ คุณจะทำให้โค้ดของคุณอ่านน้อยลงและเกิดข้อผิดพลาดได้ง่ายขึ้นโดยเฉพาะสำหรับนักพัฒนาอื่น ๆ ที่ไม่ได้สร้างรหัสของคุณ ดังนั้นเพื่อให้โค้ดของคุณอ่านง่ายและชัดเจนคุณควรเลือก C + + casts มากกว่า C-style casts
นี่คือคำพูดสั้น ๆ จากหนังสือ Bjarne Stroustrup (ผู้เขียน C ++) ภาษาการเขียนโปรแกรม C ++ รุ่นที่ 4 - หน้า 302
คาสต์สไตล์ C นี้มีอันตรายมากกว่าตัวดำเนินการแปลงที่ตั้งชื่อไว้เนื่องจากสัญกรณ์นั้นยากที่จะสังเกตเห็นในโปรแกรมขนาดใหญ่และประเภทของการแปลงที่โปรแกรมเมอร์ตั้งใจไม่ชัดเจน
เพื่อความเข้าใจลองพิจารณาตัวอย่างโค้ดด้านล่าง:
struct Foo{};
struct Bar{};
int main(int argc, char** argv)
{
Foo* f = new Foo;
Bar* b1 = f; // (1)
Bar* b2 = static_cast<Bar*>(f); // (2)
Bar* b3 = dynamic_cast<Bar*>(f); // (3)
Bar* b4 = reinterpret_cast<Bar*>(f); // (4)
Bar* b5 = const_cast<Bar*>(f); // (5)
return 0;
}
รวบรวมเฉพาะบรรทัด (4) โดยไม่มีข้อผิดพลาด reinterpret_castเท่านั้นที่สามารถใช้ในการแปลงตัวชี้ไปยังวัตถุเป็นตัวชี้ไปเป็นชนิดวัตถุใด ๆ ที่ไม่เกี่ยวข้อง
สิ่งนี้ที่ควรสังเกตคือ: dynamic_castจะล้มเหลวในเวลาทำงานอย่างไรก็ตามในคอมไพเลอร์ส่วนใหญ่มันก็จะล้มเหลวในการคอมไพล์เพราะไม่มีหน้าที่เสมือนในโครงสร้างของตัวชี้ที่กำลังส่งซึ่งหมายถึงdynamic_castจะทำงานกับพอยน์เตอร์พหุนาม .
เมื่อใดที่จะใช้ C ++ cast :
static_cast
vs dynamic_cast
vs reinterpret_cast
internals ดูบน downcast / upcast
ในคำตอบนี้ฉันต้องการเปรียบเทียบกลไกทั้งสามนี้ในตัวอย่างที่เป็นรูปธรรม upcast / downcast และวิเคราะห์สิ่งที่เกิดขึ้นกับพอยน์เตอร์ / หน่วยความจำ / แอสเซมบลีพื้นฐานเพื่อให้เข้าใจอย่างเป็นรูปธรรมเกี่ยวกับวิธีเปรียบเทียบ
ฉันเชื่อว่าสิ่งนี้จะให้สัญชาตญาณที่ดีในการปลดเปลื้องเหล่านั้นแตกต่างกัน
static_cast
: ทำการชดเชยหนึ่งแอดเดรสที่รันไทม์ (ผลกระทบจากรันไทม์ต่ำ) และไม่มีการตรวจสอบความปลอดภัยว่าดาวน์ไลท์นั้นถูกต้องหรือไม่
dyanamic_cast
: ที่อยู่เดียวกันตรงข้ามกับรันไทม์เช่นstatic_cast
กัน แต่ยังรวมถึงการตรวจสอบด้านความปลอดภัยที่มีราคาแพงว่า downcast นั้นถูกต้องโดยใช้ RTTI
การตรวจสอบความปลอดภัยนี้ช่วยให้คุณสามารถสอบถามได้ว่าตัวชี้คลาสพื้นฐานเป็นประเภทที่กำหนดเมื่อใช้งานจริงหรือไม่โดยการตรวจสอบการส่งคืนnullptr
ซึ่งบ่งชี้ว่าการดาวน์สตรีมที่ไม่ถูกต้อง
ดังนั้นหากรหัสของคุณไม่สามารถตรวจสอบได้nullptr
และดำเนินการไม่ยกเลิกที่ถูกต้องคุณควรใช้static_cast
แทนการส่งแบบไดนามิก
หากการยกเลิกเป็นการกระทำเดียวที่โค้ดของคุณสามารถทำได้คุณอาจต้องการเปิดใช้งานการdynamic_cast
ดีบักบิวด์ ( -NDEBUG
) และใช้static_cast
อย่างอื่นเช่นที่ทำที่นี่เพื่อไม่ให้การรันเร็วของคุณช้าลง
reinterpret_cast
: ไม่ทำอะไรเลยขณะรันไทม์หรือแม้แต่ออฟเซ็ตที่อยู่ ตัวชี้ต้องชี้ไปที่ประเภทที่ถูกต้องไม่แม้แต่คลาสพื้นฐานทำงาน โดยทั่วไปคุณไม่ต้องการสิ่งนี้เว้นแต่ว่ามีการสตรีมข้อมูลดิบไบต์
พิจารณาตัวอย่างรหัสต่อไปนี้:
main.cpp
#include <iostream>
struct B1 {
B1(int int_in_b1) : int_in_b1(int_in_b1) {}
virtual ~B1() {}
void f0() {}
virtual int f1() { return 1; }
int int_in_b1;
};
struct B2 {
B2(int int_in_b2) : int_in_b2(int_in_b2) {}
virtual ~B2() {}
virtual int f2() { return 2; }
int int_in_b2;
};
struct D : public B1, public B2 {
D(int int_in_b1, int int_in_b2, int int_in_d)
: B1(int_in_b1), B2(int_in_b2), int_in_d(int_in_d) {}
void d() {}
int f2() { return 3; }
int int_in_d;
};
int main() {
B2 *b2s[2];
B2 b2{11};
D *dp;
D d{1, 2, 3};
// The memory layout must support the virtual method call use case.
b2s[0] = &b2;
// An upcast is an implicit static_cast<>().
b2s[1] = &d;
std::cout << "&d " << &d << std::endl;
std::cout << "b2s[0] " << b2s[0] << std::endl;
std::cout << "b2s[1] " << b2s[1] << std::endl;
std::cout << "b2s[0]->f2() " << b2s[0]->f2() << std::endl;
std::cout << "b2s[1]->f2() " << b2s[1]->f2() << std::endl;
// Now for some downcasts.
// Cannot be done implicitly
// error: invalid conversion from ‘B2*’ to ‘D*’ [-fpermissive]
// dp = (b2s[0]);
// Undefined behaviour to an unrelated memory address because this is a B2, not D.
dp = static_cast<D*>(b2s[0]);
std::cout << "static_cast<D*>(b2s[0]) " << dp << std::endl;
std::cout << "static_cast<D*>(b2s[0])->int_in_d " << dp->int_in_d << std::endl;
// OK
dp = static_cast<D*>(b2s[1]);
std::cout << "static_cast<D*>(b2s[1]) " << dp << std::endl;
std::cout << "static_cast<D*>(b2s[1])->int_in_d " << dp->int_in_d << std::endl;
// Segfault because dp is nullptr.
dp = dynamic_cast<D*>(b2s[0]);
std::cout << "dynamic_cast<D*>(b2s[0]) " << dp << std::endl;
//std::cout << "dynamic_cast<D*>(b2s[0])->int_in_d " << dp->int_in_d << std::endl;
// OK
dp = dynamic_cast<D*>(b2s[1]);
std::cout << "dynamic_cast<D*>(b2s[1]) " << dp << std::endl;
std::cout << "dynamic_cast<D*>(b2s[1])->int_in_d " << dp->int_in_d << std::endl;
// Undefined behaviour to an unrelated memory address because this
// did not calculate the offset to get from B2* to D*.
dp = reinterpret_cast<D*>(b2s[1]);
std::cout << "reinterpret_cast<D*>(b2s[1]) " << dp << std::endl;
std::cout << "reinterpret_cast<D*>(b2s[1])->int_in_d " << dp->int_in_d << std::endl;
}
รวบรวมเรียกใช้และถอดแยกชิ้นส่วนด้วย:
g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
setarch `uname -m` -R ./main.out
gdb -batch -ex "disassemble/rs main" main.out
ที่setarch
จะใช้ในการปิดการใช้งาน ASLRเพื่อให้ง่ายต่อการเปรียบเทียบการทำงาน
เอาต์พุตที่เป็นไปได้:
&d 0x7fffffffc930
b2s[0] 0x7fffffffc920
b2s[1] 0x7fffffffc940
b2s[0]->f2() 2
b2s[1]->f2() 3
static_cast<D*>(b2s[0]) 0x7fffffffc910
static_cast<D*>(b2s[0])->int_in_d 1
static_cast<D*>(b2s[1]) 0x7fffffffc930
static_cast<D*>(b2s[1])->int_in_d 3
dynamic_cast<D*>(b2s[0]) 0
dynamic_cast<D*>(b2s[1]) 0x7fffffffc930
dynamic_cast<D*>(b2s[1])->int_in_d 3
reinterpret_cast<D*>(b2s[1]) 0x7fffffffc940
reinterpret_cast<D*>(b2s[1])->int_in_d 32767
ตอนนี้ดังที่กล่าวไว้ที่: https://en.wikipedia.org/wiki/Virtual_method_tableเพื่อสนับสนุนการเรียกใช้เมธอดเสมือนอย่างมีประสิทธิภาพโครงสร้างข้อมูลหน่วยความจำของD
ต้องมีลักษณะดังนี้:
B1:
+0: pointer to virtual method table of B1
+4: value of int_in_b1
B2:
+0: pointer to virtual method table of B2
+4: value of int_in_b2
D:
+0: pointer to virtual method table of D (for B1)
+4: value of int_in_b1
+8: pointer to virtual method table of D (for B2)
+12: value of int_in_b2
+16: value of int_in_d
ความจริงที่สำคัญคือโครงสร้างข้อมูลของหน่วยความจำD
ที่มีอยู่ข้างในโครงสร้างหน่วยความจำที่เข้ากันได้กับที่ของB1
และของB2
ภายใน
ดังนั้นเราถึงข้อสรุปที่สำคัญ:
upcast หรือ downcast เพียงต้องการเปลี่ยนค่าตัวชี้โดยค่าที่รู้จักในเวลารวบรวม
ด้วยวิธีนี้เมื่อD
ถูกส่งผ่านไปยังอาเรย์ประเภทฐานนักแสดงประเภทจะคำนวณว่าออฟเซ็ตและชี้บางสิ่งที่ดูเหมือนว่าถูกต้องB2
ในหน่วยความจำ:
b2s[1] = &d;
ยกเว้นว่าอันนี้มี vtable สำหรับD
แทนB2
และดังนั้นการโทรเสมือนทั้งหมดทำงานอย่างโปร่งใส
ตอนนี้ในที่สุดเราก็สามารถกลับไปพิมพ์แบบหล่อและวิเคราะห์ตัวอย่างที่เป็นรูปธรรมของเราได้
จากเอาต์พุต stdout เราเห็น:
&d 0x7fffffffc930
b2s[1] 0x7fffffffc940
ดังนั้นการstatic_cast
กระทำโดยปริยายจึงทำการคำนวณออฟเซ็ตจากD
โครงสร้างข้อมูลแบบเต็มอย่างถูกต้องที่ 0x7fffffffc930 ไปยังสิ่งที่B2
คล้ายกันซึ่งอยู่ที่ 0x7fffffffcc940 นอกจากนี้เรายังอนุมานว่าสิ่งที่อยู่ระหว่าง 0x7fffffffc930 และ 0x7fffffffc940 นั้นน่าจะเป็นB1
ข้อมูลและ vtable
จากนั้นในส่วน downcast ตอนนี้มันง่ายที่จะเข้าใจว่าสิ่งที่ไม่ถูกต้องล้มเหลวและทำไม:
static_cast<D*>(b2s[0]) 0x7fffffffc910
: คอมไพเลอร์เพิ่งเพิ่มขึ้น 0x10 ที่ไบต์เวลารวบรวมเพื่อลองจากB2
ไปยังที่มีD
แต่เนื่องจากb2s[0]
ไม่ใช่ a D
ตอนนี้จึงชี้ไปยังขอบเขตหน่วยความจำที่ไม่ได้กำหนด
ถอดชิ้นส่วนคือ:
49 dp = static_cast<D*>(b2s[0]);
0x0000000000000fc8 <+414>: 48 8b 45 d0 mov -0x30(%rbp),%rax
0x0000000000000fcc <+418>: 48 85 c0 test %rax,%rax
0x0000000000000fcf <+421>: 74 0a je 0xfdb <main()+433>
0x0000000000000fd1 <+423>: 48 8b 45 d0 mov -0x30(%rbp),%rax
0x0000000000000fd5 <+427>: 48 83 e8 10 sub $0x10,%rax
0x0000000000000fd9 <+431>: eb 05 jmp 0xfe0 <main()+438>
0x0000000000000fdb <+433>: b8 00 00 00 00 mov $0x0,%eax
0x0000000000000fe0 <+438>: 48 89 45 98 mov %rax,-0x68(%rbp)
ดังนั้นเราจึงเห็นว่า GCC ทำ:
D
ที่ไม่มีอยู่dynamic_cast<D*>(b2s[0]) 0
: C ++ พบจริงว่าตัวละครนั้นไม่ถูกต้องและกลับมาแล้วnullptr
!
ไม่มีวิธีนี้สามารถทำได้ในเวลารวบรวมและเราจะยืนยันว่าจากการถอดชิ้นส่วน:
59 dp = dynamic_cast<D*>(b2s[0]);
0x00000000000010ec <+706>: 48 8b 45 d0 mov -0x30(%rbp),%rax
0x00000000000010f0 <+710>: 48 85 c0 test %rax,%rax
0x00000000000010f3 <+713>: 74 1d je 0x1112 <main()+744>
0x00000000000010f5 <+715>: b9 10 00 00 00 mov $0x10,%ecx
0x00000000000010fa <+720>: 48 8d 15 f7 0b 20 00 lea 0x200bf7(%rip),%rdx # 0x201cf8 <_ZTI1D>
0x0000000000001101 <+727>: 48 8d 35 28 0c 20 00 lea 0x200c28(%rip),%rsi # 0x201d30 <_ZTI2B2>
0x0000000000001108 <+734>: 48 89 c7 mov %rax,%rdi
0x000000000000110b <+737>: e8 c0 fb ff ff callq 0xcd0 <__dynamic_cast@plt>
0x0000000000001110 <+742>: eb 05 jmp 0x1117 <main()+749>
0x0000000000001112 <+744>: b8 00 00 00 00 mov $0x0,%eax
0x0000000000001117 <+749>: 48 89 45 98 mov %rax,-0x68(%rbp)
ครั้งแรกที่มีการตรวจสอบเป็นโมฆะและมันกลับเป็นโมฆะถ้า einput ที่เป็นโมฆะ
มิฉะนั้นจะตั้งค่าการขัดแย้งบางอย่างใน RDX, RSI และ RDI __dynamic_cast
และบริการโทร
ฉันไม่มีความอดทนที่จะวิเคราะห์สิ่งนี้เพิ่มเติมในขณะนี้ แต่อย่างที่คนอื่นพูดวิธีเดียวที่จะใช้งานได้คือ__dynamic_cast
การเข้าถึงโครงสร้างข้อมูลในหน่วยความจำ RTTI พิเศษที่แสดงถึงลำดับชั้นของชั้นเรียน
ดังนั้นจึงต้องเริ่มต้นจากB2
รายการสำหรับตารางที่แล้วเดินลำดับชั้นนี้จนกว่าจะพบว่า vtable สำหรับD
typecast b2s[0]
จาก
นี่คือเหตุผลที่นักแปลตีความซ้ำอาจมีราคาแพง! ที่นี่คือตัวอย่างที่แพทช์ซับหนึ่งแปลง a dynamic_cast
เป็น a static_cast
ในโปรเจ็กต์ที่ซับซ้อนลดรันไทม์ 33%! .
reinterpret_cast<D*>(b2s[1]) 0x7fffffffc940
อันนี้แค่เชื่อว่าเราสุ่มสี่สุ่มห้า: เราบอกว่ามีD
ที่อยู่b2s[1]
และคอมไพเลอร์ไม่มีการคำนวณออฟเซ็ต
แต่นี่เป็นสิ่งที่ผิดเพราะ D จริงๆแล้วที่ 0x7fffffffc930 สิ่งที่อยู่ที่ 0x7fffffffc940 คือโครงสร้าง B2 ที่เหมือนภายใน D! ดังนั้นการเข้าถึงถังขยะ
เราสามารถยืนยันสิ่งนี้จากความสยดสยอง -O0
ชุมนุมที่ที่เพิ่งจะย้ายค่าไปรอบ ๆ :
70 dp = reinterpret_cast<D*>(b2s[1]);
0x00000000000011fa <+976>: 48 8b 45 d8 mov -0x28(%rbp),%rax
0x00000000000011fe <+980>: 48 89 45 98 mov %rax,-0x68(%rbp)
คำถามที่เกี่ยวข้อง:
ทดสอบกับ Ubuntu 18.04 amd64, GCC 7.4.0