การลบจำนวนเต็มไม่ได้ลงนามกำหนดพฤติกรรมหรือไม่


104

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

unsigned int To, Tf;

To = getcounter();
while (1) {
    Tf = getcounter();
    if ((Tf-To) >= TIME_LIMIT) {
        break;
    } 
}

นี่เป็นเพียงคำพูดที่เกี่ยวข้องอย่างคลุมเครือจากมาตรฐาน C ที่ฉันหาได้

การคำนวณที่เกี่ยวข้องกับตัวถูกดำเนินการที่ไม่ได้ลงนามไม่สามารถเกิน fl โอ๊ยได้เนื่องจากผลลัพธ์ที่ไม่สามารถแสดงโดยประเภทจำนวนเต็มที่ไม่ได้ลงนามที่เป็นผลลัพธ์จะลดโมดูโลจำนวนที่มากกว่าค่าที่มากที่สุดค่าหนึ่งที่สามารถแสดงโดยประเภทผลลัพธ์ได้

ฉันคิดว่าอาจใช้คำพูดนั้นเพื่อหมายความว่าเมื่อตัวถูกดำเนินการด้านขวามีขนาดใหญ่ขึ้นการดำเนินการจะถูกปรับให้มีความหมายในบริบทของตัวเลขที่ถูกตัดทอนโมดูโล

กล่าวคือ

0x0000 - 0x0001 == 0x 1 0000 - 0x0001 == 0xFFFF

เมื่อเทียบกับการใช้ความหมายที่ลงนามขึ้นอยู่กับการใช้งาน:

0x0000 - 0x0001 == (ไม่ได้ลงนาม) (0 + -1) == (0xFFFF แต่ยัง 0xFFFE หรือ 0x8001)

ข้อใดหรือการตีความที่ถูกต้อง? กำหนดไว้เลยหรือไม่?


3
การเลือกใช้คำในมาตรฐานเป็นเรื่องที่น่าเสียดาย "ไม่สามารถล้น" หมายความว่าไม่ใช่สถานการณ์ที่ผิดพลาด การใช้คำศัพท์ในมาตรฐานแทนที่จะใช้ค่า "wraps" มากเกินไป
danorton

คำตอบ:


109

ผลลัพธ์ของการลบที่สร้างจำนวนลบในประเภทที่ไม่ได้ลงนามนั้นมีการกำหนดไว้อย่างชัดเจน:

  1. [... ] การคำนวณที่เกี่ยวข้องกับตัวถูกดำเนินการที่ไม่ได้ลงชื่อจะไม่มีวันล้นเพราะผลลัพธ์ที่ไม่สามารถแสดงโดยประเภทจำนวนเต็มที่ไม่ได้ลงนามที่เป็นผลลัพธ์จะลดโมดูโลจำนวนที่มากกว่าค่าที่มากที่สุดค่าหนึ่งที่สามารถแสดงโดยประเภทผลลัพธ์ได้ (ISO / IEC 9899: 1999 (E) §6.2.5 / 9)

อย่างที่คุณเห็น(unsigned)0 - (unsigned)1เท่ากับ -1 โมดูโล UINT_MAX + 1 หรือกล่าวอีกนัยหนึ่งคือ UINT_MAX

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


3
ขอบคุณ! ตอนนี้ฉันเห็นการตีความที่ฉันขาดหายไป ฉันคิดว่าพวกเขาสามารถเลือกถ้อยคำที่ชัดเจนกว่านี้ได้

4
ตอนนี้ฉันรู้สึกดีขึ้นมากเมื่อรู้ว่าหากการเพิ่มที่ไม่ได้ลงนามใด ๆ หมุนไปรอบ ๆ เป็นศูนย์และทำให้เกิดการทำร้ายร่างกายอาจเป็นเพราะuintมีจุดมุ่งหมายเพื่อแสดงวงแหวนทางคณิตศาสตร์ของจำนวนเต็ม0ตลอดเวลาUINT_MAXด้วยการดำเนินการของโมดูโลการบวกและการคูณUINT_MAX+1ไม่ใช่เพราะ ของการล้น อย่างไรก็ตามขอให้ตั้งคำถามว่าเหตุใดหากเสียงเรียกเข้าเป็นประเภทข้อมูลพื้นฐานภาษาดังกล่าวจึงไม่ให้การสนับสนุนแบบทั่วไปสำหรับวงแหวนขนาดอื่น ๆ
Theodore Murdock

2
@TheodoreMurdock ฉันคิดว่าคำตอบสำหรับคำถามนั้นง่ายมาก เท่าที่ฉันสามารถบอกได้ความจริงที่ว่ามันเป็นแหวนนั้นเป็นผลที่ตามมาไม่ใช่สาเหตุ ข้อกำหนดที่แท้จริงคือประเภทที่ไม่ได้ลงชื่อจะต้องมีบิตทั้งหมดที่เข้าร่วมในการแทนค่า พฤติกรรมที่เหมือนแหวนจะไหลออกมาตามธรรมชาติ หากคุณต้องการพฤติกรรมดังกล่าวจากประเภทอื่นให้ทำเลขคณิตของคุณตามด้วยการใช้โมดูลัสที่ต้องการ ที่ใช้ตัวดำเนินการพื้นฐาน
underscore_d

@underscore_d แน่นอน ... ชัดเจนว่าทำไมพวกเขาถึงตัดสินใจออกแบบ เป็นเรื่องน่าขบขันที่พวกเขาเขียนสเป็คคร่าวๆว่า "ไม่มีเลขคณิตมากเกินไป / น้อยเกินไปเพราะประเภทข้อมูลเป็นข้อมูลจำเพาะเหมือนวงแหวน" ราวกับว่าตัวเลือกการออกแบบนี้หมายความว่าโปรแกรมเมอร์ไม่จำเป็นต้องหลีกเลี่ยงอย่างระมัดระวังมากเกินไป - ไหลหรือโปรแกรมของพวกเขาล้มเหลวอย่างไม่น่าเชื่อ
Theodore Murdock

122

เมื่อคุณทำงานกับประเภทที่ไม่ได้ลงนามจะเกิดการคำนวณทางคณิตศาสตร์แบบแยกส่วน (หรือที่เรียกว่าพฤติกรรม"ล้อมรอบ" ) หากต้องการทำความเข้าใจเกี่ยวกับการคำนวณทางคณิตศาสตร์แบบแยกส่วนเพียงแค่ดูนาฬิกาเหล่านี้:

ป้อนคำอธิบายภาพที่นี่

9 + 4 = 1 ( 13 mod 12 ) ดังนั้นในทิศทางอื่นคือ: 1 - 4 = 9 ( -3 mod 12 ) ใช้หลักการเดียวกันในขณะที่ทำงานกับประเภทที่ไม่ได้ลงชื่อ ถ้าชนิดของผลลัพธ์เป็นunsignedเลขคณิตแบบแยกส่วนจะเกิดขึ้น


ตอนนี้ดูการดำเนินการต่อไปนี้ที่จัดเก็บผลลัพธ์เป็นunsigned int:

unsigned int five = 5, seven = 7;
unsigned int a = five - seven;      // a = (-2 % 2^32) = 4294967294 

int one = 1, six = 6;
unsigned int b = one - six;         // b = (-5 % 2^32) = 4294967291

เมื่อคุณต้องการเพื่อให้แน่ใจว่าผลจะsignedเก็บไว้แล้วลงในตัวแปรหรือโยนมันทิ้งไปsigned signedเมื่อคุณต้องการได้รับความแตกต่างระหว่างตัวเลขและตรวจสอบให้แน่ใจว่าจะไม่มีการใช้เลขคณิตแบบแยกส่วนคุณควรพิจารณาใช้abs()ฟังก์ชันที่กำหนดในstdlib.h:

int c = five - seven;       // c = -2
int d = abs(five - seven);  // d =  2

ระวังให้มากโดยเฉพาะอย่างยิ่งในขณะที่เขียนเงื่อนไขเนื่องจาก:

if (abs(five - seven) < seven)  // = if (2 < 7)
    // ...

if (five - seven < -1)          // = if (-2 < -1)
    // ...

if (one - six < 1)              // = if (-5 < 1)
    // ...

if ((int)(five - seven) < 1)    // = if (-2 < 1)
    // ...

แต่

if (five - seven < 1)   // = if ((unsigned int)-2 < 1) = if (4294967294 < 1)
    // ...

if (one - six < five)   // = if ((unsigned int)-5 < 5) = if (4294967291 < 5)
    // ...

4
ดีกับนาฬิกาแม้ว่าการพิสูจน์จะทำให้คำตอบนี้ถูกต้อง หลักฐานของคำถามนั้นรวมถึงการยืนยันว่าทั้งหมดนี้อาจเป็นความจริง
Lightness Races ในวงโคจร

5
@LightnessRacesinOrbit: ขอบคุณครับ ฉันเขียนมันเพราะฉันคิดว่าอาจมีคนเห็นว่ามันเป็นประโยชน์มาก ฉันยอมรับว่ามันไม่ใช่คำตอบที่สมบูรณ์
LihO

4
สายint d = abs(five - seven);ไม่ดี ครั้งแรกfive - sevenคือการคำนวณ: ใบโปรโมชั่นประเภทตัวถูกดำเนินการเป็นunsigned intผลที่ตามมาคือการคำนวณแบบโมดูโลและประเมิน(UINT_MAX+1) UINT_MAX-1จากนั้นค่านี้คือพารามิเตอร์จริงabsซึ่งเป็นข่าวร้าย abs(int)ทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดผ่านการโต้แย้งเพราะมันไม่ได้อยู่ในช่วงและabs(long long)อาจจะสามารถเก็บค่า แต่ไม่ได้กำหนดพฤติกรรมที่เกิดขึ้นเมื่อค่าตอบแทนจะบังคับให้การเริ่มต้นint d
Ben Voigt

1
@LihO: ผู้ประกอบการเพียงใน C ++ operator T()ที่เป็นไปตามบริบทและการกระทำที่แตกต่างกันขึ้นอยู่กับว่าผลของมันจะถูกนำมาใช้เป็นผู้ประกอบการที่มีการแปลงที่กำหนดเอง การเพิ่มในสองนิพจน์ที่เรากำลังพูดถึงนั้นดำเนินการในประเภทunsigned intตามประเภทตัวถูกดำเนินการ unsigned intผลมาจากการเพิ่มคือ จากนั้นผลลัพธ์นั้นจะถูกแปลงโดยปริยายเป็นประเภทที่ต้องการในบริบทการแปลงที่ล้มเหลวเนื่องจากไม่สามารถแสดงค่าได้ในประเภทใหม่
Ben Voigt

1
@LihO: มันอาจจะช่วยในการคิดของdouble x = 2/3;VSdouble y = 2.0/3;
เบนยต์

5

การตีความครั้งแรกนั้นถูกต้อง อย่างไรก็ตามเหตุผลของคุณเกี่ยวกับ "ความหมายที่ลงนาม" ในบริบทนี้ไม่ถูกต้อง

อีกครั้งการตีความครั้งแรกของคุณถูกต้อง เลขคณิตที่ไม่ได้ลงนามเป็นไปตามกฎของเลขคณิตแบบโมดูโลซึ่งหมายความว่า0x0000 - 0x0001จะประเมินเป็น0xFFFFประเภทที่ไม่ได้ลงชื่อ 32 บิต

อย่างไรก็ตามการตีความที่สอง (การตีความตาม "ความหมายที่ลงนาม") ก็จำเป็นต้องให้ผลลัพธ์เช่นเดียวกัน กล่าวคือแม้ว่าคุณจะประเมิน0 - 1ในโดเมนของประเภทที่ลงนามและได้รับ-1เป็นผลลัพธ์ระดับกลาง แต่-1ก็ยังจำเป็นต้องสร้าง0xFFFFเมื่อภายหลังได้รับการแปลงเป็นประเภทที่ไม่ได้ลงนาม แม้ว่าบางแพลตฟอร์มจะใช้การแทนค่าที่แปลกใหม่สำหรับจำนวนเต็มที่ลงนามแล้ว (ส่วนเติมเต็ม 1, ขนาดที่ลงนาม) แต่แพลตฟอร์มนี้ยังคงต้องใช้กฎของการคำนวณทางคณิตศาสตร์แบบโมดูโลเมื่อแปลงค่าจำนวนเต็มที่ลงนามเป็นค่าที่ไม่ได้ลงนาม

ตัวอย่างเช่นการประเมินผลนี้

signed int a = 0, b = 1;
unsigned int c = a - b;

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


4
ฉันคิดว่าคุณหมายถึงประเภทที่ไม่ได้ลงชื่อ 16 บิตไม่ใช่ 32 บิต
xioxox

4

กับตัวเลขที่ได้รับการรับรองจากประเภทunsigned intหรือขนาดใหญ่ในกรณีที่ไม่มีการแปลงชนิดa-bถูกกำหนดให้เป็นผลผลิตจำนวนไม่ได้ลงนามซึ่งเมื่อเข้ามาจะให้ผลผลิตb aการแปลงจำนวนลบเป็นไม่ได้ลงนามหมายถึงการให้จำนวนซึ่งเมื่อบวกกับจำนวนเดิมที่กลับเครื่องหมายจะให้ผลเป็นศูนย์ (ดังนั้นการแปลง -5 เป็นไม่ได้ลงนามจะให้ค่าซึ่งเมื่อเพิ่มเป็น 5 จะให้ผลเป็นศูนย์) .

หมายเหตุว่าตัวเลขที่ได้รับการรับรองมีขนาดเล็กกว่าunsigned intอาจได้รับการเลื่อนตำแหน่งให้เป็นประเภทintก่อนที่จะลบพฤติกรรมของจะขึ้นอยู่กับขนาดของa-bint

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