เหตุใดเครื่องมือเพิ่มประสิทธิภาพ GCC 6 ที่ปรับปรุงแล้วจึงใช้งาน C ++ ได้จริง


148

GCC 6 มีคุณลักษณะเครื่องมือเพิ่มประสิทธิภาพใหม่ : จะถือว่าthisไม่เป็นโมฆะและปรับให้เหมาะสมตามนั้น

ตอนนี้การแพร่กระจายช่วงค่าถือว่าตัวชี้ของฟังก์ชันสมาชิก C ++ นี้ไม่ใช่ค่าว่าง กำจัดนี้ null ร่วมกันตรวจสอบตัวชี้แต่ยังแบ่งบางส่วนไม่สอดคล้องรหัสฐาน (เช่น Qt-5, โครเมี่ยม, KDevelop) สามารถใช้การแก้ไขชั่วคราว -fno-delete-null-pointer-checks ชั่วคราวได้ รหัสที่ไม่ถูกต้องสามารถระบุได้โดยใช้ -fsanitize = undefined

เอกสารการเปลี่ยนแปลงเรียกสิ่งนี้ว่าเป็นอันตรายอย่างชัดเจนเพราะแบ่งรหัสที่ใช้บ่อยเป็นจำนวนมากอย่างน่าประหลาดใจ

เหตุใดสมมติฐานใหม่นี้จึงไม่ใช้งานรหัส C ++ ที่ใช้งานได้จริง มีรูปแบบเฉพาะหรือไม่ที่โปรแกรมเมอร์ไม่ประมาทหรือไม่มีข้อมูลจะพึ่งพาพฤติกรรมที่ไม่ได้กำหนดนี้หรือไม่? ฉันไม่สามารถจินตนาการได้ว่ามีใครเขียนif (this == NULL)เพราะมันแปลกประหลาดมาก


21
@Ben หวังว่าคุณจะหมายถึงมันในทางที่ดี รหัสที่มี UB ควรเขียนใหม่ไม่ให้เรียกใช้ UB มันง่ายอย่างที่คิด เฮคมักมีคำถามที่พบบ่อยซึ่งบอกวิธีการทำให้สำเร็จ ดังนั้นไม่ใช่เรื่องจริง IMHO ทั้งหมดดี.
Reinstate Monica

49
ฉันประหลาดใจที่เห็นผู้คนปกป้องตัวชี้โมฆะ dereferencing ในรหัส น่าอัศจรรย์เพียง
SergeyA

19
@Ben การสำรวจพฤติกรรมที่ไม่ได้กำหนดเป็นกลยุทธ์การเพิ่มประสิทธิภาพที่มีประสิทธิภาพมากเป็นเวลานาน ฉันรักมันเพราะฉันชอบการเพิ่มประสิทธิภาพซึ่งทำให้โค้ดของฉันทำงานได้เร็วขึ้น
SergeyA

17
ฉันเห็นด้วยกับ SergeyA brouhaha ทั้งหมดเริ่มต้นเพราะคนดูเหมือนจะอยู่กับความจริงที่thisถูกส่งผ่านเป็นพารามิเตอร์โดยปริยายดังนั้นพวกเขาจึงเริ่มใช้มันราวกับว่ามันเป็นพารามิเตอร์ที่ชัดเจน มันไม่ใช่. เมื่อคุณทำการอ้างอิงค่า Null นี้คุณจะเรียกใช้ UB เสมือนว่าคุณได้ทำการตรวจสอบตัวชี้ Null อื่น ๆ นั่นคือทั้งหมดที่มีไป หากคุณต้องการที่จะผ่าน nullptrs รอบใช้พารามิเตอร์ชัดเจน DUH มันจะไม่ช้าลงมันจะไม่เป็น clunkier ใด ๆ และรหัสที่มี API ดังกล่าวอยู่ลึกเข้าไปใน internals อยู่แล้วดังนั้นจึงมีขอบเขตที่ จำกัด มาก จบเรื่องที่ฉันคิด
Reinstate Monica

41
ความรุ่งโรจน์ถึง GCC สำหรับการทำลายวงจรของรหัสที่ไม่ถูกต้อง -> คอมไพเลอร์ที่ไม่มีประสิทธิภาพเพื่อสนับสนุนโค้ดที่ไม่ดี -> โค้ดที่แย่กว่า -> การรวบรวมที่ไม่มีประสิทธิภาพมากขึ้น -> ...
MM

คำตอบ:


87

ฉันเดาคำถามที่ต้องตอบว่าทำไมคนที่มีเจตนาดีจะเขียนเช็คในตอนแรก

กรณีที่พบบ่อยที่สุดคือถ้าคุณมีคลาสที่เป็นส่วนหนึ่งของการโทรซ้ำที่เกิดขึ้นตามธรรมชาติ

ถ้าคุณมี:

struct Node
{
    Node* left;
    Node* right;
};

ใน C คุณอาจเขียน:

void traverse_in_order(Node* n) {
    if(!n) return;
    traverse_in_order(n->left);
    process(n);
    traverse_in_order(n->right);
}

ใน C ++ มันเป็นการดีที่จะทำให้ฟังก์ชั่นนี้เป็นสมาชิก:

void Node::traverse_in_order() {
    // <--- What check should be put here?
    left->traverse_in_order();
    process();
    right->traverse_in_order();
}

ในวันแรก ๆ ของ C ++ (ก่อนมาตรฐาน) มันถูกเน้นว่าหน้าที่ของสมาชิกนั้นเป็นน้ำตาลซินแทคติคสำหรับฟังก์ชั่นที่มีthisพารามิเตอร์โดยปริยาย รหัสถูกเขียนใน C ++, แปลงเป็น C เทียบเท่าและเรียบเรียง มีตัวอย่างที่ชัดเจนว่าการเปรียบเทียบthisกับ null มีความหมายและคอมไพเลอร์ Cfront ดั้งเดิมก็ใช้ประโยชน์จากสิ่งนี้เช่นกัน ดังนั้นมาจากพื้นหลัง C ตัวเลือกที่ชัดเจนสำหรับการตรวจสอบคือ:

if(this == nullptr) return;      

หมายเหตุ: Bjarne Stroustrup ยังระบุว่ากฎสำหรับการthisเปลี่ยนแปลงในช่วงหลายปีที่นี่

และนี่ใช้กับคอมไพเลอร์หลาย ๆ ตัวเป็นเวลาหลายปี เมื่อมาตรฐานเกิดขึ้นสิ่งนี้ก็เปลี่ยนไป และเมื่อเร็ว ๆ คอมไพเลอร์เริ่มต้นการใช้ประโยชน์จากการเรียกฟังก์ชันสมาชิกที่thisเป็นอยู่nullptrเป็นพฤติกรรมที่ไม่ได้กำหนดไว้ซึ่งหมายความว่าสภาพเช่นนี้อยู่เสมอfalseและคอมไพเลอร์ที่เป็นอิสระที่จะละเว้นมัน

นั่นหมายความว่าคุณต้องทำการสำรวจเส้นทางของต้นไม้นี้คุณต้อง:

  • ตรวจสอบทั้งหมดก่อนโทร traverse_in_order

    void Node::traverse_in_order() {
        if(left) left->traverse_in_order();
        process();
        if(right) right->traverse_in_order();
    }

    นี่หมายถึงการตรวจสอบที่ไซต์การโทรทุกคนหากคุณมีรูทว่าง

  • อย่าใช้ฟังก์ชั่นสมาชิก

    ซึ่งหมายความว่าคุณกำลังเขียนรหัสสไตล์ C เก่า (อาจเป็นวิธีการคงที่) และเรียกมันด้วยวัตถุอย่างชัดเจนเป็นพารามิเตอร์ เช่น. คุณกลับไปเขียนNode::traverse_in_order(node);แทนที่จะไปnode->traverse_in_order();ที่ไซต์โทร

  • ผมเชื่อว่าวิธีที่ง่ายที่สุด / neatest nullptrในการแก้ไขปัญหาเช่นนี้โดยเฉพาะในทางที่เป็นไปตามมาตรฐานที่จะใช้งานจริงโหนดแมวมองมากกว่าหนึ่ง

    // static class, or global variable
    Node sentinel;
    
    void Node::traverse_in_order() {
        if(this == &sentinel) return;
        ...
    }

ไม่มีตัวเลือกสองตัวเลือกแรกที่ดูเหมือนว่าน่าสนใจและในขณะที่โค้ดอาจหายไปกับมันพวกเขาเขียนโค้ดไม่ดีด้วยthis == nullptrแทนที่จะใช้การแก้ไขที่เหมาะสม

ฉันเดาว่านี่เป็นวิธีที่รหัสฐานบางส่วนมีวิวัฒนาการเพื่อให้มีการthis == nullptrตรวจสอบ


6
วิธีการอาจ1 == 0จะไม่ได้กำหนดพฤติกรรม? falseมันเป็นเพียงแค่
Johannes Schaub - litb

11
การตรวจสอบตัวเองไม่ใช่พฤติกรรมที่ไม่ได้กำหนด มันเป็นเท็จเสมอและกำจัดโดยคอมไพเลอร์
SergeyA

15
อืม .. this == nullptrสำนวนเป็นพฤติกรรมที่ไม่ได้กำหนดเพราะคุณได้เรียกใช้ฟังก์ชันสมาชิกบนวัตถุ nullptr ก่อนหน้านั้นซึ่งไม่ได้กำหนด และคอมไพเลอร์มีอิสระที่จะละเว้นการตรวจสอบ
jtlim

6
@ โจชัวมาตรฐานแรกที่ตีพิมพ์ในปี 1998 สิ่งที่เกิดขึ้นก่อนหน้านั้นคือสิ่งที่การดำเนินการทุกอย่างที่ต้องการ ยุคมืด.
SergeyA

26
Heh ว้าวฉันไม่สามารถเชื่อว่าทุกคนที่เคยเขียนรหัสที่อาศัยในการเรียกฟังก์ชั่นเช่น ... โดยไม่ต้องอินสแตนซ์ ฉันจะใช้ข้อความที่ตัดตอนมาโดยสัญชาตญาณว่า "ตรวจสอบทั้งหมดก่อนที่จะโทรไปที่ traverse_in_order" โดยไม่ต้องคิดthisเลยว่าจะเป็นโมฆะ ฉันเดาว่านี่อาจจะเป็นประโยชน์ในการเรียนรู้ C ++ ในยุคที่มีอยู่เพื่อปกป้องความอันตรายของ UB ในสมองของฉันและห้ามปรามฉันจากการทำแฮ็กแปลก ๆ เช่นนี้
underscore_d

65

มันเป็นเช่นนั้นเพราะรหัส "ในทางปฏิบัติ" เสียและเกี่ยวข้องกับพฤติกรรมที่ไม่ได้กำหนดเพื่อเริ่มต้นด้วย ไม่มีเหตุผลที่จะใช้เป็นโมฆะthisอื่น ๆ นอกเหนือจากการเพิ่มประสิทธิภาพขนาดเล็กมักจะเป็นหนึ่งก่อนวัยอันควร

เป็นการปฏิบัติที่อันตรายเนื่องจากการปรับพอยน์เตอร์เนื่องจากการเลื่อนผ่านลำดับชั้นของคลาสสามารถเปลี่ยนค่าว่างให้เป็นค่าthisที่ไม่เป็นค่าว่างได้ อย่างน้อยที่สุดคลาสที่วิธีการทำงานกับ null thisจะต้องเป็นคลาสสุดท้ายที่ไม่มีคลาสพื้นฐาน: มันไม่สามารถได้มาจากสิ่งใดและมันไม่ได้มาจาก เรากำลังได้อย่างรวดเร็วออกจากการปฏิบัติเพื่อให้น่าเกลียดสับที่ดิน

ในทางปฏิบัติรหัสไม่ต้องน่าเกลียด:

struct Node
{
  Node* left;
  Node* right;
  void process();
  void traverse_in_order() {
    traverse_in_order_impl(this);
  }
private:
  static void traverse_in_order_impl(Node * n)
    if (!n) return;
    traverse_in_order_impl(n->left);
    n->process();
    traverse_in_order_impl(n->right);
  }
};

หากคุณมีต้นไม้ว่างเปล่า (เช่น root เป็น nullptr) โซลูชันนี้ยังคงใช้พฤติกรรมที่ไม่ได้กำหนดโดยการเรียกใช้ traverse_in_order ด้วย nullptr

หากทรีว่างเปล่าหรือที่เรียกว่าโมฆะNode* rootคุณไม่ควรเรียกวิธีการใด ๆ ที่ไม่คงที่ ระยะเวลา เป็นการดีที่จะมีรหัสต้นไม้แบบ C ที่ใช้ตัวชี้อินสแตนซ์โดยพารามิเตอร์ที่ชัดเจน

อาร์กิวเมนต์ที่นี่ดูเหมือนว่าจะเดือดลงไปอย่างใดจำเป็นต้องเขียนวิธีการไม่คงที่บนวัตถุที่สามารถเรียกได้จากตัวชี้ตัวอย่างเช่นโมฆะ ไม่มีความต้องการเช่นนั้น วิธีการเขียนโค้ดแบบ C-with-objects ยังคงเป็นวิธีที่ดีกว่าในโลก C ++ เพราะสามารถพิมพ์ได้อย่างปลอดภัยอย่างน้อยที่สุด โดยพื้นฐานแล้ว null thisเป็นเช่นการเพิ่มประสิทธิภาพขนาดเล็กด้วยการใช้งานในพื้นที่แคบ ๆ ที่ไม่อนุญาตให้ IMHO ปรับได้อย่างสมบูรณ์ ไม่มีประชาชน API thisควรขึ้นอยู่กับโมฆะ


18
@Ben ใครก็ตามที่เขียนรหัสนี้ผิดตั้งแต่แรก เป็นเรื่องตลกที่คุณกำลังตั้งชื่อโครงการที่พังยับเยินเช่น MFC, Qt และ Chromium ความดีกับพวกเขา
SergeyA

19
@Ben ฉันรู้จักสไตล์การเขียนโค้ดที่แย่มากใน Google รหัส Google (อย่างน้อยคนทั่วไป) มักจะเขียนไม่ดีแม้ว่าหลายคนเชื่อว่าโค้ด Google เป็นตัวอย่างที่ส่องแสง อาจเป็นสิ่งนี้จะทำให้พวกเขากลับมาใช้รูปแบบการเข้ารหัสของพวกเขาอีกครั้ง (และแนวทางในขณะที่พวกเขาอยู่ในนั้น)
SergeyA

18
@Ben ไม่มีใครแทนที่ Chromium ย้อนหลังบนอุปกรณ์เหล่านี้ด้วย Chromium ที่คอมไพล์ด้วย gcc 6 ก่อนที่ Chromium จะถูกคอมไพล์โดยใช้ gcc 6 และคอมไพเลอร์สมัยใหม่อื่น ๆ จะต้องได้รับการแก้ไข มันไม่ใช่งานที่ยิ่งใหญ่เช่นกัน การthisตรวจสอบจะถูกเลือกโดยเครื่องวิเคราะห์รหัสแบบคงที่หลาย ๆ ตัวดังนั้นจึงไม่เหมือนกับว่ามีใครต้องตามล่าพวกมันทั้งหมดด้วยตนเอง แพทช์น่าจะเป็นการเปลี่ยนแปลงเล็กน้อยสองร้อยบรรทัด
Reinstate Monica

8
@Ben ในแง่ปฏิบัติข้อบกพร่องที่เป็นโมฆะthisเป็นความผิดพลาดทันที ปัญหาเหล่านี้จะถูกค้นพบอย่างรวดเร็วแม้ว่าจะไม่มีใครสนใจที่จะรันตัววิเคราะห์แบบคงที่เหนือรหัส C / C ++ ทำตามคำว่า "จ่ายเฉพาะสำหรับคุณสมบัติที่คุณใช้" หากคุณต้องการตรวจสอบคุณจะต้องชัดเจนเกี่ยวกับพวกเขาและนั่นหมายความว่าไม่ได้ทำthisมันเมื่อมันสายเกินไปเนื่องจากคอมไพเลอร์ถือว่าthisไม่เป็นโมฆะ มิฉะนั้นจะต้องตรวจสอบthisและสำหรับรหัส 99.9999% การตรวจสอบดังกล่าวจะเสียเวลา
Reinstate Monica

10
คำแนะนำของฉันสำหรับทุกคนที่คิดว่ามาตรฐานใช้งานไม่ได้: ใช้ภาษาอื่น ไม่มีปัญหาการขาดแคลนภาษา C ++ เหมือนภาษาที่ไม่มีความเป็นไปได้ของพฤติกรรมที่ไม่ได้กำหนด
MM

35

เอกสารการเปลี่ยนแปลงเรียกสิ่งนี้ว่าเป็นอันตรายอย่างชัดเจนเพราะแบ่งรหัสที่ใช้บ่อยเป็นจำนวนมากอย่างน่าประหลาดใจ

เอกสารไม่ได้เรียกว่าเป็นอันตราย หรือไม่ก็อ้างว่าจะแบ่งเป็นจำนวนเงินที่น่าแปลกใจของรหัส มันชี้ให้เห็นเพียงไม่กี่รหัสฐานที่เป็นที่นิยมซึ่งมันอ้างว่าเป็นที่รู้จักที่จะพึ่งพาพฤติกรรมที่ไม่ได้กำหนดนี้และจะแตกเนื่องจากการเปลี่ยนแปลงเว้นแต่จะมีการใช้ตัวเลือกการแก้ปัญหา

เหตุใดสมมติฐานใหม่นี้จึงไม่ใช้งานรหัส C ++ ที่ใช้งานได้จริง

หากรหัส c ++ ที่ใช้งานได้จริงขึ้นอยู่กับพฤติกรรมที่ไม่ได้กำหนดการเปลี่ยนแปลงพฤติกรรมที่ไม่ได้กำหนดนั้นสามารถทำลายได้ นี่คือสาเหตุที่ต้องหลีกเลี่ยง UB แม้ว่าโปรแกรมที่ใช้โปรแกรมจะทำงานได้ตามที่ตั้งใจ

มีรูปแบบเฉพาะหรือไม่ที่โปรแกรมเมอร์ไม่ประมาทหรือไม่มีข้อมูลจะพึ่งพาพฤติกรรมที่ไม่ได้กำหนดนี้หรือไม่?

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

if (this)
    member_variable = 42;

เมื่อบั๊กที่เกิดขึ้นจริงถูกยกเลิกการอ้างอิงตัวชี้โมฆะที่อื่น

ฉันแน่ใจว่าถ้าโปรแกรมเมอร์ไม่มีข้อมูลเพียงพอพวกเขาจะสามารถสร้างรูปแบบที่ก้าวหน้ากว่า (ต่อต้าน) ที่พึ่งพา UB นี้ได้

ฉันไม่สามารถจินตนาการได้ว่ามีใครเขียนif (this == NULL)เพราะมันแปลกประหลาดมาก

ฉันสามารถ.


11
"ถ้ารหัส c ++ ที่ใช้งานจริงนั้นขึ้นอยู่กับพฤติกรรมที่ไม่ได้กำหนดการเปลี่ยนแปลงพฤติกรรมที่ไม่ได้กำหนดนั้นอาจทำให้แตกได้นี่คือสาเหตุที่ UB ต้องหลีกเลี่ยง" นี่ * 1,000
underscore_d

if(this == null) PrintSomeHelpfulDebugInformationAboutHowWeGotHere(); เช่นบันทึกที่อ่านง่ายของลำดับเหตุการณ์ที่ดีบักเกอร์ไม่สามารถบอกคุณได้อย่างง่ายดาย สนุกกับการแก้ไขข้อบกพร่องในตอนนี้โดยไม่ต้องใช้เวลาวางเช็คทุกที่เมื่อมีค่าสุ่มแบบสุ่มในชุดข้อมูลขนาดใหญ่ในรหัสที่คุณไม่ได้เขียน ... และกฎ UB เกี่ยวกับเรื่องนี้ถูกสร้างขึ้นในภายหลังหลังจาก C ++ ถูกสร้างขึ้น มันเคยถูกต้อง
Stephane Hockenhull

@StephaneHockenhull นั่นคือสิ่งที่-fsanitize=nullมีไว้เพื่อ
eerorika

@ user2079303 ปัญหา: นั่นจะทำให้รหัสการผลิตช้าลงจนถึงจุดที่คุณไม่สามารถออกจากการเช็คอินในขณะที่ทำงานทำให้ บริษัท ต้องเสียเงินจำนวนมากหรือไม่ นั่นจะเป็นการเพิ่มขนาดและไม่พอดีกับแฟลชหรือไม่? ใช้งานได้กับแพลตฟอร์มเป้าหมายทั้งหมดรวมถึง Atmel หรือไม่ สามารถ-fsanitize=nullบันทึกข้อผิดพลาดไปยังการ์ด SD / MMC บนพิน # 5,6,10,11 โดยใช้ SPI ได้หรือไม่ นั่นไม่ใช่ทางออกสากล บางคนแย้งว่ามันขัดกับหลักการเชิงวัตถุในการเข้าถึงวัตถุที่เป็นโมฆะ แต่ภาษา OOP บางภาษามีวัตถุ null ที่สามารถใช้งานได้ดังนั้นจึงไม่ใช่กฎสากลของ OOP 1/2
Stephane Hockenhull

1
... การแสดงออกปกติซึ่งตรงกับไฟล์ดังกล่าว? การพูดว่าหากมีการเข้าถึง lvalue สองครั้งคอมไพเลอร์อาจรวมการเข้าถึงยกเว้นว่าโค้ดระหว่างสิ่งเหล่านี้ทำสิ่งต่าง ๆ ที่เฉพาะเจาะจงหลายอย่างจะง่ายกว่าการพยายามกำหนดสถานการณ์ที่แม่นยำซึ่งรหัสได้รับอนุญาตให้เข้าถึงที่เก็บข้อมูล
supercat

25

โค้ด "ใช้งานจริง" (วิธีตลก ๆ ในการสะกด "buggy") ที่ใช้งานไม่ได้มีลักษณะดังนี้:

void foo(X* p) {
  p->bar()->baz();
}

และมันลืมที่จะอธิบายถึงความจริงที่ว่าp->bar()บางครั้งส่งกลับตัวชี้โมฆะซึ่งหมายความว่าการยกเลิกการลงทะเบียนเพื่อโทรbaz()นั้นไม่ได้กำหนดไว้

ไม่ใช่รหัสทั้งหมดที่มีข้อผิดพลาดอย่างชัดเจนif (this == nullptr)หรือมีการif (!p) return;ตรวจสอบ บางกรณีเป็นเพียงฟังก์ชั่นที่ไม่สามารถเข้าถึงตัวแปรสมาชิกใด ๆ และดูเหมือนว่าจะทำงานได้ดี ตัวอย่างเช่น:

struct DummyImpl {
  bool valid() const { return false; }
  int m_data;
};
struct RealImpl {
  bool valid() const { return m_valid; }
  bool m_valid;
  int m_data;
};

template<typename T>
void do_something_else(T* p) {
  if (p) {
    use(p->m_data);
  }
}

template<typename T>
void func(T* p) {
  if (p->valid())
    do_something(p);
  else 
    do_something_else(p);
}

ในรหัสนี้เมื่อคุณเรียกfunc<DummyImpl*>(DummyImpl*)ตัวชี้โมฆะมี "ความคิด" ของ dereference ตัวชี้การเรียกร้องp->DummyImpl::valid()แต่ในความจริงที่ว่าฟังก์ชันสมาชิกเพียงผลตอบแทนโดยไม่ต้องเข้าถึงfalse *thisที่return falseสามารถ inline และในทางปฏิบัติตัวชี้ไม่จำเป็นต้องเข้าถึงเลย ดังนั้นด้วยคอมไพเลอร์บางตัวก็ดูเหมือนว่าจะทำงานได้ดี: ไม่มี segfault สำหรับ dereferencing null, p->valid()เป็นเท็จ, ดังนั้นการเรียกรหัสdo_something_else(p), ซึ่งตรวจสอบพอยน์เตอร์พอยน์เตอร์, และไม่ทำอะไรเลย. ไม่พบความผิดพลาดหรือพฤติกรรมที่ไม่คาดคิด

ด้วย GCC 6 คุณยังคงได้รับสายp->valid()แต่ตอนนี้คอมไพเลอร์ infers จากการแสดงออกที่pจะต้องไม่เป็นโมฆะ (มิฉะนั้นp->valid()จะเป็นพฤติกรรมที่ไม่ได้กำหนด) และทำการบันทึกข้อมูลนั้น ข้อมูลที่สรุปนั้นถูกใช้โดยเครื่องมือเพิ่มประสิทธิภาพดังนั้นหากการเรียกเพื่อdo_something_else(p)รับอินไลน์การif (p)ตรวจสอบจะถูกพิจารณาซ้ำซ้อนเนื่องจากคอมไพเลอร์จดจำว่าไม่เป็นโมฆะและอินไลน์รหัสเพื่อ:

template<typename T>
void func(T* p) {
  if (p->valid())
    do_something(p);
  else {
    // inlined body of do_something_else(p) with value propagation
    // optimization performed to remove null check.
    use(p->m_data);
  }
}

ตอนนี้จะทำการตรวจสอบตัวชี้โมฆะจริง ๆ ดังนั้นรหัสที่ปรากฏก่อนหน้านี้จะหยุดทำงาน

ในตัวอย่างนี้มีข้อบกพร่องfuncซึ่งควรตรวจสอบเป็นโมฆะก่อน (หรือผู้โทรไม่ควรเรียกว่าเป็นโมฆะ):

template<typename T>
void func(T* p) {
  if (p && p->valid())
    do_something(p);
  else 
    do_something_else(p);
}

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


มันเป็นไปได้ที่จะตราสาร GCC 6 เพื่อส่งออกคำเตือนรวบรวมเวลาเมื่อพบประเพณีดังกล่าวthis?
jotik


3
@jotik, ^^^ สิ่งที่ TC พูด มันจะเป็นไปได้ แต่คุณจะได้รับการเตือนว่าFOR ALL รหัสทุกเวลา การเผยแพร่ช่วงค่าเป็นหนึ่งในการปรับปรุงที่พบมากที่สุดซึ่งมีผลต่อรหัสเกือบทุกที่ทุกที่ เครื่องมือเพิ่มประสิทธิภาพเพียงแค่ดูรหัสซึ่งสามารถทำให้ง่ายขึ้น พวกเขาไม่เห็น "ชิ้นส่วนของรหัสที่เขียนโดยคนงี่เง่าที่ต้องการได้รับการเตือนหาก UB ที่เป็นใบ้ของพวกเขาได้รับการปรับให้เหมาะสม" ไม่ใช่เรื่องง่ายสำหรับคอมไพเลอร์ที่จะบอกความแตกต่างระหว่าง "การตรวจสอบซ้ำซ้อนว่าโปรแกรมเมอร์ต้องการปรับให้เหมาะสม" และ "การตรวจสอบซ้ำซ้อนที่โปรแกรมเมอร์คิดว่าจะช่วยได้ แต่ซ้ำซ้อน"
Jonathan Wakely

1
หากคุณต้องการที่จะใช้รหัสของคุณเพื่อให้ข้อผิดพลาดรันไทม์สำหรับ UB ประเภทต่างๆรวมถึงการใช้งานที่ไม่ถูกต้องthisเพียงใช้-fsanitize=undefined
Jonathan Wakely


-25

มาตรฐาน C ++ แตกในวิธีการสำคัญ น่าเสียดายที่แทนที่จะปกป้องผู้ใช้จากปัญหาเหล่านี้นักพัฒนา GCC ได้เลือกที่จะใช้พฤติกรรมที่ไม่ได้กำหนดเป็นข้ออ้างในการใช้การปรับให้เหมาะสมที่สุดแม้ว่าจะได้รับการอธิบายอย่างชัดเจนว่าเป็นอันตรายอย่างไร

นี่เป็นคนที่ฉลาดกว่าที่ฉันอธิบายอย่างละเอียดมาก (เขากำลังพูดถึง C แต่สถานการณ์ก็เหมือนกัน)

ทำไมจึงเป็นอันตราย

เพียงแค่คอมไพล์ใหม่ก่อนหน้านี้ทำงานรหัสรักษาความปลอดภัยกับคอมไพเลอร์รุ่นใหม่สามารถแนะนำช่องโหว่ความปลอดภัยก่อนหน้านี้ทำงานรหัสความปลอดภัยกับรุ่นใหม่ของคอมไพเลอร์สามารถนำช่องโหว่ความปลอดภัย ในขณะที่พฤติกรรมใหม่สามารถปิดการใช้งานด้วยการตั้งค่าสถานะ, makefiles ที่มีอยู่ไม่ได้ตั้งค่าการตั้งค่าที่แน่นอน และเนื่องจากไม่มีคำเตือนใด ๆ เกิดขึ้นจึงไม่ชัดเจนนักพัฒนาที่พฤติกรรมที่เหมาะสมก่อนหน้านี้ได้เปลี่ยนไป

ในตัวอย่างนี้นักพัฒนาได้รวมการตรวจสอบจำนวนเต็มล้นโดยใช้assertซึ่งจะยุติโปรแกรมหากมีการระบุความยาวที่ไม่ถูกต้อง ทีม GCC นำการตรวจสอบออกตามที่จำนวนเต็มล้นไม่ได้กำหนดดังนั้นการตรวจสอบสามารถลบออกได้ สิ่งนี้ส่งผลให้อินสแตนซ์ในปัจจุบันที่แท้จริงของโค้ดเบสนี้ถูกทำขึ้นใหม่โดยมีความเสี่ยงหลังจากปัญหาได้รับการแก้ไขแล้ว

อ่านทั้งหมด มันเพียงพอที่จะทำให้คุณร้องไห้

ตกลง แต่แล้วอันนี้ล่ะ

ย้อนกลับไปเมื่อมีสำนวนที่ค่อนข้างธรรมดาซึ่งไปบางอย่างเช่นนี้:

 OPAQUEHANDLE ObjectType::GetHandle(){
    if(this==NULL)return DEFAULTHANDLE;
    return mHandle;

 }

 void DoThing(ObjectType* pObj){
     osfunction(pObj->GetHandle(), "BLAH");
 }

ดังนั้นสำนวนคือ: ถ้าpObjไม่ใช่โมฆะคุณใช้ที่จับที่มีอยู่มิฉะนั้นคุณจะใช้ที่จับเริ่มต้น นี่คือการห่อหุ้มในGetHandleฟังก์ชั่น

เคล็ดลับคือการเรียกใช้ฟังก์ชันที่ไม่ใช่เสมือนจริงไม่ได้ใช้ประโยชน์จากthisตัวชี้ดังนั้นจึงไม่มีการละเมิดการเข้าถึง

ฉันยังไม่เข้าใจ

มีโค้ดจำนวนมากที่เขียนเช่นนั้น หากมีใครคอมไพล์ใหม่อีกครั้งโดยไม่ต้องเปลี่ยนสายการโทรทุกครั้งจะDoThing(NULL)เป็นข้อผิดพลาดหากคุณโชคดี

หากคุณไม่โชคดีการโทรหาจุดบกพร่องจะกลายเป็นช่องโหว่ของการเรียกใช้จากระยะไกล

สิ่งนี้สามารถเกิดขึ้นได้โดยอัตโนมัติ คุณมีระบบสร้างอัตโนมัติใช่มั้ย การอัพเกรดเป็นคอมไพเลอร์ล่าสุดนั้นไม่เป็นอันตรายใช่ไหม? แต่ตอนนี้มันไม่ใช่ - ไม่ใช่ถ้าคอมไพเลอร์ของคุณคือ GCC

ตกลงเพื่อบอกพวกเขา!

พวกเขาได้รับการบอกเล่า พวกเขากำลังทำสิ่งนี้อย่างเต็มความรู้เกี่ยวกับผลที่ตามมา

แต่ ... ทำไม

ใครสามารถพูดได้ บางทีอาจจะเป็น:

  • พวกเขาให้ความสำคัญกับความบริสุทธิ์ในอุดมคติของภาษา C ++ มากกว่ารหัสจริง
  • พวกเขาเชื่อว่าผู้คนควรได้รับการลงโทษหากไม่ปฏิบัติตามมาตรฐาน
  • พวกเขาไม่เข้าใจความเป็นจริงของโลก
  • พวกเขาคือ ... แนะนำบั๊กตามวัตถุประสงค์ บางทีสำหรับรัฐบาลต่างประเทศ คุณอาศัยอยู่ที่ไหน? รัฐบาลทั้งหมดนั้นเป็นของต่างประเทศไปยังส่วนใหญ่ของโลกและส่วนใหญ่เป็นศัตรูกับบางส่วนของโลก

หรืออาจเป็นอย่างอื่น ใครสามารถพูดได้


32
ไม่เห็นด้วยกับคำตอบทุกบรรทัดเดียว มีการแสดงความคิดเห็นแบบเดียวกันสำหรับการเพิ่มประสิทธิภาพการสร้างสมนามอย่างเข้มงวดและผู้ที่หวังว่าจะถูกยกเลิกในตอนนี้ การแก้ปัญหาคือให้ความรู้แก่นักพัฒนาไม่ป้องกันการปรับให้เหมาะสมตามนิสัยการพัฒนาที่ไม่ดี
SergeyA

30
ฉันไปและอ่านทุกสิ่งอย่างที่คุณพูดและฉันก็ร้องไห้ แต่ส่วนใหญ่ที่ความโง่เขลาของเฟลิกซ์ซึ่งฉันไม่คิดว่าเป็นสิ่งที่คุณพยายามจะข้าม ...
Mike Vine

33
ลดลงสำหรับการคุยโวไร้ประโยชน์ "พวกเขาคือ ... นำเสนอข้อบกพร่องตามวัตถุประสงค์บางทีสำหรับรัฐบาลต่างประเทศ" จริงๆ? นี่ไม่ใช่ / การสมคบคิด /
isanae

31
โปรแกรมเมอร์ที่มีคุณค่าซ้ำแล้วซ้ำอีกซ้ำมนต์ไม่ได้เรียกพฤติกรรมที่ไม่ได้กำหนดแต่ nonks เหล่านี้ได้ไปข้างหน้าและทำมันต่อไป และดูสิ่งที่เกิดขึ้น ฉันไม่มีความเห็นอกเห็นใจใด ๆ นี่เป็นความผิดของนักพัฒนาง่าย ๆ พวกเขาต้องรับผิดชอบ จำไว้? ความรับผิดชอบส่วนบุคคล? ผู้คนที่พึ่งพามนต์ของคุณ "แต่สิ่งที่เกี่ยวกับการปฏิบัติ !" เป็นสถานการณ์ที่เกิดขึ้นในครั้งแรกอย่างแม่นยำ การหลีกเลี่ยงเรื่องไร้สาระเช่นนี้เป็นเหตุผลว่าทำไมมาตรฐานจึงมีอยู่ตั้งแต่แรก รหัสมาตรฐานและคุณจะไม่มีปัญหา ระยะเวลา
Lightness Races ในวงโคจร

18
"เพียงแค่ recompiling ก่อนหน้านี้ทำงานรหัสความปลอดภัยกับรุ่นใหม่ของคอมไพเลอร์สามารถนำช่องโหว่ความปลอดภัย" - ที่เกิดขึ้นเสมอ นอกจากว่าคุณต้องการมอบอำนาจให้คอมไพเลอร์หนึ่งเวอร์ชันเป็นคอมไพเลอร์เดียวที่จะได้รับอนุญาตสำหรับส่วนที่เหลือของนิรันดร คุณจำได้ไหมว่าเคอร์เนล linux สามารถคอมไพล์ด้วย gcc 2.7.2.1 เท่านั้น โครงการจีซีซีถึงกับถูกแยกจากกันเพราะผู้คนต่างก็เบื่อหน่ายกับพลั่ว มันใช้เวลานานกว่าจะผ่านมา
MM
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.