C มีค่า std :: น้อยกว่าจาก C ++ หรือไม่


26

ฉันเพิ่งตอบคำถามเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดของการทำp < qใน C เมื่อpและqเป็นตัวชี้ไปยังวัตถุ / อาร์เรย์ที่แตกต่างกัน นั่นทำให้ฉันคิดว่า: C ++ มีพฤติกรรมเหมือนกัน (ไม่ได้กำหนด) <ในกรณีนี้ แต่ยังมีเท็มเพลตไลบรารีมาตรฐานstd::lessซึ่งรับประกันว่าจะส่งคืนสิ่งเดียวกันกับ<ที่สามารถเปรียบเทียบตัวชี้และส่งคืนการสั่งซื้อบางอย่างที่ไม่สอดคล้อง

C เสนอบางสิ่งที่มีฟังก์ชั่นคล้ายกันซึ่งจะช่วยให้เปรียบเทียบตัวชี้ตามอำเภอใจ (กับประเภทเดียวกัน) ได้อย่างปลอดภัยหรือไม่? ฉันพยายามมองผ่านมาตรฐาน C11 และไม่พบอะไรเลย แต่ประสบการณ์ของฉันใน C คือขนาดที่เล็กกว่า C ++ ดังนั้นฉันอาจจะพลาดอะไรบางอย่างได้อย่างง่ายดาย


1
ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Samuel Liew

คำตอบ:


20

ในการใช้งานกับโมเดลหน่วยความจำแบบแบน (โดยพื้นฐานแล้วทุกอย่าง) การส่งไปที่uintptr_tJust Work

(แต่ดูการเปรียบเทียบตัวชี้ควรลงนามหรือไม่ได้ลงนามใน 64- บิต x86 หรือไม่สำหรับการอภิปรายว่าคุณควรปฏิบัติต่อพอยน์เตอร์ตามที่ลงชื่อหรือไม่รวมถึงปัญหาในการสร้างพอยน์เตอร์นอกวัตถุซึ่งเป็น UB ใน C. )

แต่ระบบที่มีรุ่นหน่วยความจำไม่แบนทำอยู่และคิดเกี่ยวกับพวกเขาสามารถช่วยอธิบายสถานการณ์ปัจจุบันเช่น C ++ มีรายละเอียดที่แตกต่างกันสำหรับเทียบกับ<std::less


ส่วนหนึ่งของจุด<บนตัวชี้ไปยังวัตถุที่แยกเป็น UB ใน C (หรืออย่างน้อยไม่ได้ระบุในการแก้ไข C ++ บางส่วน) คือการอนุญาตให้เครื่องแปลก ๆ รวมถึงรุ่นหน่วยความจำที่ไม่ใช่แบน

ตัวอย่างที่รู้จักกันดีคือโหมดจริง x86-16 ที่ชี้เป็นส่วน: ชดเชยในรูปแบบ 20 (segment << 4) + offsetบิตเชิงเส้นที่อยู่ผ่านทาง ที่อยู่เชิงเส้นเดียวกันสามารถแสดงโดยชุดค่าผสม seg: off แบบหลายค่าได้

C ++ std::lessสำหรับพอยน์เตอร์บน ISAs แปลก ๆ อาจต้องมีราคาแพงเช่น "ปกติ" เซกเมนต์: ออฟเซ็ตบน x86-16 เพื่อออฟเซ็ต <= 15 อย่างไรก็ตามไม่มีวิธีพกพาในการติดตั้ง การจัดการที่จำเป็นในการทำให้เป็นปกติuintptr_t(หรือการแทนวัตถุของวัตถุตัวชี้) เป็นการใช้งานเฉพาะ

แต่ถึงแม้ในระบบที่ C ++ std::lessต้องมีราคาแพง<ก็ไม่จำเป็นต้องเป็น ตัวอย่างเช่นสมมติว่าโมเดลหน่วยความจำ "ใหญ่" ที่วัตถุควรอยู่ในส่วนเดียว<สามารถเปรียบเทียบส่วนออฟเซ็ตและไม่ต้องกังวลกับส่วนเซ็กเมนต์ (ตัวชี้ภายในวัตถุเดียวกันจะมีเซ็กเมนต์เดียวกันและมิฉะนั้นจะเป็น UB ใน C. C ++ 17 เปลี่ยนเป็น "ไม่ระบุ" เท่านั้นซึ่งอาจอนุญาตให้ข้ามการทำให้เป็นมาตรฐานและเปรียบเทียบการออฟเซ็ตได้) นี่เป็นการสมมติพอยน์เตอร์ทุกส่วน ของวัตถุจะใช้segค่าเดียวกันเสมอไม่ทำให้ปกติ นี่คือสิ่งที่คุณคาดหวังให้ ABI ต้องการสำหรับ "ใหญ่" ซึ่งตรงข้ามกับโมเดลหน่วยความจำ "ใหญ่" (ดูการอภิปรายในความคิดเห็น )

(โมเดลหน่วยความจำดังกล่าวอาจมีขนาดวัตถุสูงสุด 64kiB แต่พื้นที่ที่อยู่รวมสูงสุดที่ใหญ่กว่าซึ่งมีที่ว่างสำหรับวัตถุขนาดใหญ่ที่สุดเช่นนี้ ISO C ช่วยให้การใช้งานมีขีด จำกัด ของขนาดวัตถุที่ต่ำกว่า ค่าสูงสุด (ไม่ได้ลงนาม) size_tสามารถเป็นตัวแทนSIZE_MAXได้ตัวอย่างเช่นแม้ในระบบแบบจำลองหน่วยความจำแบบแบน GNU C จำกัด ขนาดของวัตถุให้ใหญ่สุดเพื่อPTRDIFF_MAXให้การคำนวณขนาดสามารถเพิกเฉยต่อการโอเวอร์โฟลว์ที่ลงชื่อแล้ว) ดูคำตอบและการอภิปราย

หากคุณต้องการอนุญาตให้วัตถุมีขนาดใหญ่กว่าเซกเมนต์คุณต้องมีโมเดลหน่วยความจำ "ใหญ่" ที่ต้องกังวลเกี่ยวกับการโอเวอร์โฟลว์ส่วนออฟเซ็ตของตัวชี้เมื่อทำการp++วนลูปผ่านอาร์เรย์หรือเมื่อทำการคำนวณดัชนี / ตัวชี้ทางคณิตศาสตร์ สิ่งนี้นำไปสู่โค้ดที่ช้าลงทุกที่ แต่อาจหมายถึงว่าp < qจะเกิดขึ้นกับพอยน์เตอร์ไปยังวัตถุต่าง ๆ เนื่องจากการใช้งานที่กำหนดเป้าหมายโมเดล "หน่วยความจำขนาดใหญ่" โดยปกติแล้วจะเลือกให้พอยน์เตอร์ ดูพอยน์เตอร์ที่อยู่ใกล้ไกลและไกลขนาดไหน - คอมไพเลอร์ C ตัวจริงสำหรับโหมดจริง x86 มีตัวเลือกในการคอมไพล์สำหรับโมเดล "ใหญ่" ที่พอยน์เตอร์เริ่มต้นทั้งหมดเป็น "ใหญ่" เว้นแต่จะมีการประกาศเป็นอย่างอื่น

x86 การแบ่งส่วนโหมดจริงไม่ใช่รูปแบบหน่วยความจำแบบไม่แบนเท่านั้นที่เป็นไปได้มันเป็นเพียงตัวอย่างที่เป็นประโยชน์เพื่อแสดงให้เห็นว่ามันได้รับการจัดการอย่างไรโดยการประยุกต์ใช้ C / C ++ ในชีวิตจริงการใช้งานขยาย ISO C ด้วยแนวคิดของfarvs. nearpointers ทำให้โปรแกรมเมอร์สามารถเลือกได้เมื่อพวกเขาสามารถออกไปได้ด้วยการเก็บ / ส่งผ่านส่วนออฟเซ็ต 16 บิตเทียบกับส่วนข้อมูลทั่วไปบางส่วน

แต่การใช้งาน ISO C บริสุทธิ์นั้นจะต้องเลือกระหว่างโมเดลหน่วยความจำขนาดเล็ก (ทุกอย่างยกเว้นรหัสใน 64kiB เดียวกันกับตัวชี้ 16 บิต) หรือใหญ่หรือใหญ่โดยตัวชี้ทั้งหมดเป็น 32 บิต บางลูปสามารถปรับให้เหมาะสมโดยการเพิ่มเฉพาะส่วนออฟเซ็ต แต่วัตถุตัวชี้ไม่สามารถปรับให้เล็กลงได้


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

หรืออาจไม่ใช่: มันอาจเกี่ยวข้องกับการค้นหาบางสิ่งบางอย่างจากตารางเซ็กเมนต์พิเศษหรือบางสิ่งบางอย่างเช่นโหมดที่ได้รับการป้องกัน x86 แทนโหมดจริงที่ส่วนของที่อยู่เซ็กเมนต์นั้นเป็นดัชนีไม่ใช่ค่าที่จะเลื่อนไป คุณสามารถตั้งค่าส่วนที่ทับซ้อนกันบางส่วนในโหมดที่ได้รับการป้องกันและส่วนตัวเลือกส่วนของที่อยู่ไม่จำเป็นต้องเรียงลำดับตามที่อยู่เดียวกันกับที่อยู่ฐานของส่วนที่เกี่ยวข้อง การรับที่อยู่เชิงเส้นจากตัวชี้ seg: off ในโหมดที่ได้รับการป้องกัน x86 อาจเกี่ยวข้องกับการเรียกของระบบหาก GDT และ / หรือ LDT ไม่ได้รับการแมปลงในหน้าเว็บที่อ่านได้ในกระบวนการของคุณ

(แน่นอนว่าระบบปฏิบัติการหลักสำหรับ x86 ใช้โมเดลหน่วยความจำแบบแบนดังนั้นฐานเซ็กเมนต์จะเป็น 0 เสมอ (ยกเว้นหน่วยเก็บข้อมูลเธรดโลคัลที่ใช้fsหรือgsเซ็กเมนต์) และใช้เฉพาะส่วน "ออฟเซ็ต" แบบ 32 บิตหรือ 64 บิตเป็นตัวชี้ .)

คุณสามารถเพิ่มรหัสสำหรับแพลตฟอร์มเฉพาะต่าง ๆ ด้วยตนเองเช่นโดยค่าเริ่มต้นถือว่าแบนหรือ#ifdefสิ่งที่จะตรวจจับโหมด x86 จริงและแบ่งออกuintptr_tเป็นครึ่ง 16 บิตseg -= off>>4; off &= 0xf;จากนั้นรวมชิ้นส่วนเหล่านั้นกลับเป็นจำนวน 32 บิต


เหตุใดจึงเป็น UB หากกลุ่มไม่เท่ากัน
โอ๊ก

@Acorn: หมายถึงการพูดแบบอื่น ๆ ; แก้ไขแล้ว. พอยน์เตอร์ในวัตถุเดียวกันจะมีเซ็กเมนต์เดียวกันมิฉะนั้น UB
Peter Cordes

แต่ทำไมคุณถึงคิดว่ามันเป็น UB ในทุกกรณี? (ฤvertedษีตรรกะหรือไม่จริงฉันไม่ได้สังเกตเห็นอย่างใดอย่างหนึ่ง)
โอ๊ก

p < qเป็น UB ใน C หรือไม่หากพวกเขาชี้ไปที่วัตถุอื่นใช่มั้ย ฉันรู้ว่าp - qเป็น
Peter Cordes

1
@Acorn: อย่างไรก็ตามฉันไม่เห็นกลไกที่จะสร้างนามแฝง (seg ที่แตกต่าง: ปิดที่อยู่เชิงเส้นเดียวกัน) ในโปรแกรมที่ไม่มี UB ดังนั้นจึงไม่เหมือนที่ผู้แปลต้องหลีกทางให้ได้ การเข้าถึงวัตถุทุกครั้งจะใช้segค่าของวัตถุนั้นและการชดเชยที่> = การชดเชยภายในส่วนที่วัตถุนั้นเริ่มต้น C ทำให้ UB จะทำอะไรมากระหว่างตัวชี้ไปยังวัตถุที่แตกต่างกันรวมทั้งสิ่งที่ชอบtmp = a-bแล้วในการเข้าถึงb[tmp] a[0]การอภิปรายเกี่ยวกับนามแฝงของตัวชี้แบ่งกลุ่มเป็นตัวอย่างที่ดีว่าเหตุใดตัวเลือกการออกแบบจึงสมเหตุสมผล
Peter Cordes

17

ฉันเคยพยายามหาวิธีแก้ไขปัญหานี้และหาวิธีแก้ปัญหาที่ทับซ้อนกับวัตถุและในกรณีอื่น ๆ ส่วนใหญ่สมมติว่าคอมไพเลอร์ทำสิ่ง "ปกติ"

คุณสามารถใช้ข้อเสนอแนะในวิธีการใช้ memmove ในมาตรฐาน C โดยไม่ต้องคัดลอกกลาง? และถ้าหากมันไม่สามารถใช้งานได้กับuintptr(ประเภท wrapper สำหรับอย่างใดอย่างหนึ่งuintptr_tหรือunsigned long longขึ้นอยู่กับว่าuintptr_tมี) และได้ผลลัพธ์ที่แม่นยำที่สุด (แม้ว่ามันอาจจะไม่สำคัญก็ตาม):

#include <stdint.h>
#ifndef UINTPTR_MAX
typedef unsigned long long uintptr;
#else
typedef uintptr_t uintptr;
#endif

int pcmp(const void *p1, const void *p2, size_t len)
{
    const unsigned char *s1 = p1;
    const unsigned char *s2 = p2;
    size_t l;

    /* Check for overlap */
    for( l = 0; l < len; l++ )
    {
        if( s1 + l == s2 || s1 + l == s2 + len - 1 )
        {
            /* The two objects overlap, so we're allowed to
               use comparison operators. */
            if(s1 > s2)
                return 1;
            else if (s1 < s2)
                return -1;
            else
                return 0;
        }
    }

    /* No overlap so the result probably won't really matter.
       Cast the result to `uintptr` and hope the compiler
       does the "usual" thing */
    if((uintptr)s1 > (uintptr)s2)
        return 1;
    else if ((uintptr)s1 < (uintptr)s2)
        return -1;
    else
        return 0;
}

5

C นำเสนอบางสิ่งด้วยฟังก์ชันการทำงานที่คล้ายกันซึ่งจะอนุญาตให้เปรียบเทียบตัวชี้โดยพลการอย่างปลอดภัยหรือไม่

ไม่


ก่อนอื่นให้เราพิจารณาพอยน์เตอร์พอยน์เตอร์เท่านั้น พอยน์เตอร์ของฟังก์ชั่นนำมาซึ่งความกังวลอีกชุด

2 พอยน์เตอร์p1, p2สามารถมีการเข้ารหัสที่แตกต่างกันและชี้ไปยังที่อยู่เดียวกันดังนั้นp1 == p2แม้ว่าmemcmp(&p1, &p2, sizeof p1)จะไม่ใช่ 0 สถาปัตยกรรมดังกล่าวหายาก

แต่การแปลงของตัวชี้เหล่านี้เพื่อไม่ต้องใช้ผลจำนวนเต็มเดียวกันที่นำไปสู่ uintptr_t(uintptr_t)p1 != (uinptr_t)p2

(uintptr_t)p1 < (uinptr_t)p2 ตัวเองเป็นรหัสทางกฎหมายโดยอาจไม่ให้ความหวังสำหรับการทำงาน


หากรหัสต้องการเปรียบเทียบพอยน์เตอร์ที่ไม่เกี่ยวข้องอย่างแท้จริงให้สร้างฟังก์ชันผู้ช่วยless(const void *p1, const void *p2)และดำเนินการตามรหัสเฉพาะแพลตฟอร์มที่นั่น

บางที:

// return -1,0,1 for <,==,> 
int ptrcmp(const void *c1, const void *c1) {
  // Equivalence test works on all platforms
  if (c1 == c2) {
    return 0;
  }
  // At this point, we know pointers are not equivalent.
  #ifdef UINTPTR_MAX
    uintptr_t u1 = (uintptr_t)c1;
    uintptr_t u2 = (uintptr_t)c2;
    // Below code "works" in that the computation is legal,
    //   but does it function as desired?
    // Likely, but strange systems lurk out in the wild. 
    // Check implementation before using
    #if tbd
      return (u1 > u2) - (u1 < u2);
    #else
      #error TBD code
    #endif
  #else
    #error TBD code
  #endif 
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.