เมื่อใดที่การเรียกใช้ฟังก์ชันสมาชิกบนอินสแตนซ์ว่างจะทำให้เกิดพฤติกรรมที่ไม่ได้กำหนด


120

พิจารณารหัสต่อไปนี้:

#include <iostream>

struct foo
{
    // (a):
    void bar() { std::cout << "gman was here" << std::endl; }

    // (b):
    void baz() { x = 5; }

    int x;
};

int main()
{
    foo* f = 0;

    f->bar(); // (a)
    f->baz(); // (b)
}

เราคาดว่า(b)จะขัดข้องเนื่องจากไม่มีสมาชิกที่เกี่ยวข้องxสำหรับตัวชี้ค่าว่าง ในทางปฏิบัติ(a)ไม่ผิดพลาดเนื่องจากthisไม่เคยใช้ตัวชี้

เนื่องจาก(b)dereferences thisตัวชี้ ( (*this).x = 5;) และthisเป็นโมฆะโปรแกรมจึงเข้าสู่พฤติกรรมที่ไม่ได้กำหนดเนื่องจากการยกเลิกการอ้างอิง null มักจะกล่าวว่าเป็นพฤติกรรมที่ไม่ได้กำหนด

ไม่(a)ส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนด? จะเกิดอะไรขึ้นถ้าทั้งสองฟังก์ชัน (และx) เป็นแบบคงที่?


ถ้าฟังก์ชันทั้งสองเป็นแบบคงที่ x จะอ้างถึงภายในbaz ได้อย่างไร? (x เป็นตัวแปรสมาชิกที่ไม่คงที่)
legends2k

4
@ legends2k: แกล้งทำเป็นxนิ่งเกินไป :)
GManNickG

แน่นอน แต่สำหรับกรณี (a) มันทำงานเหมือนกันในทุกกรณีกล่าวคือฟังก์ชันจะถูกเรียกใช้ อย่างไรก็ตามการแทนที่ค่าของตัวชี้จาก 0 เป็น 1 (พูดผ่าน reinterpret_cast) มันมักจะขัดข้องเสมอ การจัดสรรค่าเป็น 0 และเป็นโมฆะในกรณีที่ a แสดงถึงสิ่งพิเศษสำหรับคอมไพเลอร์หรือไม่? เหตุใดจึงมีข้อผิดพลาดกับค่าอื่น ๆ ที่จัดสรรให้เสมอ
Siddharth Shankaran

5
สิ่งที่น่าสนใจ: ในการแก้ไข C ++ ครั้งต่อไปจะไม่มีการอ้างอิงตัวชี้อีกต่อไป ตอนนี้เราจะดำเนินการตามทิศทางผ่านพอยน์เตอร์ หากต้องการข้อมูลเพิ่มเติมโปรดดำเนินการตามทิศทาง
James McNellis

3
การเรียกใช้ฟังก์ชันสมาชิกบนตัวชี้ค่าว่างเป็นพฤติกรรมที่ไม่ได้กำหนดไว้เสมอ เพียงแค่ดูรหัสของคุณฉันก็รู้สึกได้ถึงพฤติกรรมที่ไม่ได้กำหนดค่อยๆคลานขึ้นมาที่คอของฉัน!
fredoverflow

คำตอบ:


113

ทั้งสองอย่าง(a)และ(b)ส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนด พฤติกรรมที่ไม่ได้กำหนดไว้เสมอในการเรียกใช้ฟังก์ชันสมาชิกผ่านตัวชี้ค่าว่าง หากฟังก์ชันเป็นแบบคงที่แสดงว่าไม่ได้กำหนดไว้ในทางเทคนิคเช่นกัน แต่มีข้อโต้แย้งบางประการ


สิ่งแรกที่ต้องทำความเข้าใจคือเหตุใดพฤติกรรมที่ไม่ได้กำหนดไว้ในการหักล้างตัวชี้ค่าว่าง ใน C ++ 03 มีความคลุมเครืออยู่เล็กน้อยที่นี่

แม้ว่า"การยกเลิกการอ้างอิงตัวชี้ค่าว่างจะทำให้เกิดพฤติกรรมที่ไม่ได้กำหนด"ในบันทึกย่อทั้ง§1.9 / 4 และ§8.3.2 / 4 แต่ก็ไม่เคยระบุไว้อย่างชัดเจน (หมายเหตุไม่ใช่กฎเกณฑ์)

อย่างไรก็ตามเราสามารถลองอนุมานได้จาก§3.10 / 2:

lvalue หมายถึงวัตถุหรือฟังก์ชัน

เมื่อยกเลิกการอ้างอิงผลลัพธ์จะเป็นค่า lvalue ตัวชี้ค่าว่างไม่ได้อ้างถึงวัตถุดังนั้นเมื่อเราใช้ lvalue เราจึงมีพฤติกรรมที่ไม่ได้กำหนดไว้ ปัญหาคือไม่เคยระบุประโยคก่อนหน้าดังนั้นการ "ใช้" lvalue หมายความว่าอย่างไร? เพียงแค่สร้างมันขึ้นมาเลยหรือจะใช้ในความหมายที่เป็นทางการมากขึ้นของการแปลง lvalue-to-rvalue?

ไม่ว่าจะไม่สามารถแปลงเป็นค่า r ((4.1 / 1) ได้แน่นอน:

หากอ็อบเจ็กต์ที่ lvalue อ้างถึงไม่ใช่อ็อบเจ็กต์ประเภท T และไม่ใช่อ็อบเจ็กต์ประเภทที่มาจาก T หรือถ้าอ็อบเจ็กต์ไม่ได้กำหนดค่าเริ่มต้นโปรแกรมที่จำเป็นต้องมีการแปลงนี้จะมีพฤติกรรมที่ไม่ได้กำหนดไว้

นี่คือพฤติกรรมที่ไม่ได้กำหนดไว้อย่างแน่นอน

ความคลุมเครือมาจากพฤติกรรมที่ไม่ได้กำหนดเป็นการเบี่ยงเบนหรือไม่แต่ไม่ใช้ค่าจากตัวชี้ที่ไม่ถูกต้อง (นั่นคือรับค่า lvalue แต่ไม่แปลงเป็นค่า rvalue) ถ้าไม่เช่นนั้นก็int *i = 0; *i; &(*i);มีการกำหนดไว้อย่างดี นี่เป็นปัญหาที่เกิดขึ้น

ดังนั้นเราจึงมีมุมมอง "dereference a null pointer ที่ไม่ได้กำหนดพฤติกรรมที่ไม่ได้กำหนด" และจุดอ่อน "ใช้ตัวชี้ค่า null ที่ไม่ได้อ้างอิงได้รับมุมมองพฤติกรรมที่ไม่ได้กำหนด"

ตอนนี้เราพิจารณาคำถาม


ใช่(a)ส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนด ในความเป็นจริงถ้าthisเป็นโมฆะโดยไม่คำนึงถึงเนื้อหาของฟังก์ชันผลลัพธ์จะไม่ได้กำหนด

สิ่งนี้มาจาก§5.2.5 / 3:

หากE1มีประเภท "ตัวชี้เป็นคลาส X" นิพจน์E1->E2จะถูกแปลงเป็นรูปแบบที่เทียบเท่า(*(E1)).E2;

*(E1)จะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดด้วยการตีความที่เข้มงวดและ.E2แปลงเป็นค่า r ทำให้เป็นพฤติกรรมที่ไม่ได้กำหนดไว้สำหรับการตีความที่อ่อนแอ

นอกจากนี้ยังเป็นไปตามพฤติกรรมที่ไม่ได้กำหนดโดยตรงจาก (§9.3.1 / 1):

ถ้าฟังก์ชันสมาชิกที่ไม่คงที่ของคลาส X ถูกเรียกใช้สำหรับอ็อบเจ็กต์ที่ไม่ใช่ประเภท X หรือประเภทที่มาจาก X พฤติกรรมนั้นจะไม่ได้กำหนดไว้


ด้วยฟังก์ชันคงที่การตีความที่เข้มงวดและอ่อนแอทำให้เกิดความแตกต่าง พูดอย่างเคร่งครัดไม่ได้กำหนด:

สมาชิกแบบคงที่อาจถูกอ้างถึงโดยใช้ไวยากรณ์การเข้าถึงของสมาชิกคลาสซึ่งในกรณีนี้จะมีการประเมินนิพจน์อ็อบเจ็กต์

นั่นคือมันเป็นเพียงการประเมินราวกับว่ามันเป็นไม่คงที่และเราอีกครั้ง dereference (*(E1)).E2ชี้โมฆะด้วย

อย่างไรก็ตามเนื่องจากE1ไม่ได้ใช้ในการเรียกฟังก์ชันสมาชิกแบบคงที่ถ้าเราใช้การแปลความหมายที่ไม่ชัดเจนการเรียกจะถูกกำหนดไว้อย่างดี *(E1)ผลลัพธ์ใน lvalue ฟังก์ชันคงที่ได้รับการแก้ไข*(E1)ถูกละทิ้งและฟังก์ชันนี้ถูกเรียกใช้ ไม่มีการแปลงค่า lvalue-to-rvalue ดังนั้นจึงไม่มีพฤติกรรมที่ไม่ได้กำหนด

ใน C ++ 0x ณ n3126 ความคลุมเครือยังคงอยู่ ตอนนี้ปลอดภัย: ใช้การตีความอย่างเคร่งครัด


5
+1 การอวดรู้อย่างต่อเนื่องภายใต้ "คำจำกัดความที่อ่อนแอ" ฟังก์ชัน nonstatic member ไม่ได้ถูกเรียก "สำหรับวัตถุที่ไม่ใช่ประเภท X" มันถูกเรียกว่า lvalue ซึ่งไม่ใช่วัตถุเลย ดังนั้นโซลูชันที่เสนอจึงเพิ่มข้อความ "หรือถ้า lvalue เป็น lvalue ว่าง" ลงในประโยคที่คุณอ้าง
Steve Jessop

คุณช่วยชี้แจงเล็กน้อยได้ไหม โดยเฉพาะกับลิงก์ "ปัญหาที่ปิด" และ "ปัญหาที่ใช้งานอยู่" หมายเลขปัญหาคืออะไร นอกจากนี้หากเป็นปัญหาแบบปิดคำตอบใช่ / ไม่ใช่สำหรับฟังก์ชันคงที่คืออะไร? ฉันรู้สึกว่าฉันพลาดขั้นตอนสุดท้ายในการพยายามทำความเข้าใจคำตอบของคุณ
Brooks Moses

4
ฉันไม่คิดว่าข้อบกพร่อง CWG 315 นั้น "ปิด" เนื่องจากการมีอยู่ในหน้า "ปัญหาที่ปิด" เป็นนัย เหตุผลบอกว่าควรได้รับอนุญาตเนื่องจาก " *pไม่ใช่ข้อผิดพลาดเมื่อpเป็นโมฆะเว้นแต่ lvalue จะถูกแปลงเป็น rvalue" อย่างไรก็ตามนั่นขึ้นอยู่กับแนวคิดของ "ค่า lvalue ที่ว่างเปล่า" ซึ่งเป็นส่วนหนึ่งของการแก้ปัญหาที่เสนอให้กับข้อบกพร่องของ CWG 232แต่ยังไม่ได้นำมาใช้ ดังนั้นด้วยภาษาทั้งใน C ++ 03 และ C ++ 0x การยกเลิกการอ้างอิงตัวชี้ null จึงยังไม่ได้กำหนดแม้ว่าจะไม่มีการแปลง lvalue-to-rvalue ก็ตาม
James McNellis

1
@JamesMcNellis: ตามความเข้าใจของฉันถ้าpเป็นที่อยู่ฮาร์ดแวร์ซึ่งจะทำให้เกิดการดำเนินการบางอย่างเมื่ออ่าน แต่ไม่ได้รับการประกาศvolatileคำสั่ง*p;จะไม่จำเป็นแต่จะได้รับอนุญาตให้อ่านที่อยู่นั้นจริงๆ &(*p);อย่างไรก็ตามคำสั่งจะถูกห้ามมิให้ทำเช่นนั้น ถ้า*pเป็นvolatileเช่นนั้นจำเป็นต้องอ่าน ไม่ว่าในกรณีใดถ้าตัวชี้ไม่ถูกต้องฉันไม่เห็นว่าคำสั่งแรกจะไม่ใช่พฤติกรรมที่ไม่ได้กำหนดได้อย่างไร แต่ฉันก็ไม่เห็นว่าเหตุใดคำสั่งที่สองจึงเป็นเช่นนั้น
supercat

1
".E2 แปลงเป็นค่า rvalue" - เอ่อไม่ไม่
MM

30

แน่นอนไม่ได้กำหนดหมายความว่าไม่ได้กำหนดไว้แต่บางครั้งก็สามารถคาดเดาได้ ข้อมูลที่ฉันกำลังจะให้ไม่ควรใช้สำหรับโค้ดที่ใช้งานได้เนื่องจากไม่มีการรับประกันอย่างแน่นอน แต่อาจมีประโยชน์เมื่อทำการดีบัก

คุณอาจคิดว่าการเรียกใช้ฟังก์ชันบนตัวชี้วัตถุจะทำให้ตัวชี้ผิดปกติและทำให้เกิด UB ในทางปฏิบัติถ้าฟังก์ชั่นที่ไม่ได้เป็นเสมือนคอมไพเลอร์จะมีการแปลงไปเรียกฟังก์ชันธรรมดาผ่านตัวชี้เป็นพารามิเตอร์แรกนี้ผ่าน dereference และการสร้างระเบิดครั้งสำหรับการที่เรียกว่าฟังก์ชั่นสมาชิก หากฟังก์ชันสมาชิกไม่ได้อ้างอิงตัวแปรสมาชิกหรือฟังก์ชันเสมือนจริงฟังก์ชันนี้อาจประสบความสำเร็จโดยไม่มีข้อผิดพลาด จำไว้ว่าการประสบความสำเร็จนั้นตกอยู่ในจักรวาลของ "ไม่ได้กำหนด"!

ฟังก์ชัน MFC ของ Microsoft GetSafeHwndอาศัยพฤติกรรมนี้จริงๆ ฉันไม่รู้ว่าพวกเขาสูบบุหรี่เพราะอะไร

หากคุณกำลังเรียกใช้ฟังก์ชันเสมือนตัวชี้จะต้องถูกยกเลิกการอ้างอิงเพื่อไปที่ vtable และแน่นอนว่าคุณจะได้รับ UB (อาจเป็นข้อขัดข้อง แต่อย่าลืมว่าไม่มีการรับประกัน)


1
ก่อนอื่น GetSafeHwnd จะทำการตรวจสอบ! และถ้าเป็นจริงจะส่งคืนค่า NULL จากนั้นจะเริ่มกรอบ SEH และยกเลิกการอ้างอิงตัวชี้ หากมีการละเมิดการเข้าถึงหน่วยความจำ (0xc0000005) สิ่งนี้ถูกจับได้และ NULL จะถูกส่งกลับไปยังผู้โทร :) อื่น HWND จะถูกส่งกลับ
ПетърПетров

@ ПетърПетровเป็นเวลาไม่กี่ปีแล้วที่ฉันดูรหัสGetSafeHwndเป็นไปได้ว่าพวกเขาได้ปรับปรุงมันตั้งแต่นั้นมา และอย่าลืมว่าพวกเขามีความรู้ภายในเกี่ยวกับการทำงานของคอมไพเลอร์!
Mark Ransom

ฉันกำลังระบุตัวอย่างการใช้งานที่เป็นไปได้ที่มีผลเหมือนกันสิ่งที่ทำได้คือการทำวิศวกรรมย้อนกลับโดยใช้ดีบักเกอร์ :)
ПетърПетров

1
"พวกเขามีความรู้ภายในเกี่ยวกับการทำงานของคอมไพเลอร์!" - สาเหตุของปัญหาชั่วนิรันดร์สำหรับโครงการเช่น MinGW ที่พยายามอนุญาตให้ g ++ รวบรวมโค้ดที่เรียก Windows API
MM

@MM ฉันคิดว่าเราทุกคนคงเห็นด้วยว่านี่ไม่ยุติธรรม และด้วยเหตุนี้ฉันจึงคิดว่ามีกฎหมายเกี่ยวกับความเข้ากันได้ที่ทำให้ผิดกฎหมายเล็กน้อยที่จะเก็บไว้เช่นนั้น
v.oddou
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.