เหตุใดตัวทำซ้ำมาตรฐานจึงมีช่วง [เริ่มต้น, สิ้นสุด] แทนที่จะเป็น [เริ่มต้น, สิ้นสุด]


204

เหตุใดมาตรฐานจึงกำหนดend()เป็นจุดสิ้นสุดที่ผ่านมาแทนที่จะเป็นที่จุดสิ้นสุดจริง


19
ฉันเดา "เพราะนั่นคือสิ่งที่มาตรฐานบอกว่า" จะไม่ตัดใช่มั้ย :)
Luchian Grigore

39
@ LuchianGrigore: ไม่แน่นอน นั่นจะกัดเซาะความเคารพของเราสำหรับ (คนที่อยู่เบื้องหลัง) มาตรฐาน เราควรคาดหวังว่ามีเหตุผลสำหรับตัวเลือกที่ทำโดยมาตรฐาน
Kerrek SB

4
ในระยะสั้นคอมพิวเตอร์ไม่นับเหมือนคน แต่ถ้าคุณอยากรู้ว่าทำไมผู้คนถึงไม่นับเหมือนคอมพิวเตอร์ฉันไม่แนะนำอะไรเลย: ประวัติศาสตร์ธรรมชาติของศูนย์สำหรับการมองในเชิงลึกถึงปัญหาที่มนุษย์พบว่ามีจำนวนน้อยกว่า มากกว่าหนึ่ง
John McFarlane

8
เนื่องจากมีวิธีเดียวในการสร้าง "ล่าสุด" จึงมักจะไม่ถูกเพราะต้องเป็นจริง การสร้าง "คุณหลุดออกจากปลายหน้าผา" นั้นราคาถูกเสมอตัวแทนที่เป็นไปได้จำนวนมากจะทำ (เป็นโมฆะ *) "ahhhhhhh" จะทำได้ดี
ฮันส์ Passant

6
ฉันดูวันที่ของคำถามและในวินาทีนั้นฉันคิดว่าคุณล้อเล่น
Asaf

คำตอบ:


286

ข้อโต้แย้งที่ดีที่สุดคือสิ่งที่Dijkstraทำเอง :

  • คุณต้องการให้ขนาดของช่วงเป็นจุดสิ้นสุดที่แตกต่างง่าย ๆ -  เริ่มต้น ;

  • รวมถึงขอบเขตที่ต่ำกว่านั้นเป็น "ธรรมชาติ" เมื่อลำดับลดลงเป็นค่าว่างและเนื่องจากทางเลือก ( ไม่รวมขอบเขตล่าง) จะต้องมีค่า Sentinel ของ "หนึ่ง - ก่อน - ต้น - เริ่ม"

คุณยังต้องพิสูจน์ว่าทำไมคุณถึงเริ่มนับที่ศูนย์มากกว่าหนึ่ง แต่นั่นไม่ใช่คำถามของคุณ

ภูมิปัญญาที่อยู่เบื้องหลังการประชุม [เริ่มต้นสิ้นสุด) จะเป็นการสละเวลาและอีกครั้งเมื่อคุณมีอัลกอริทึมใด ๆ ที่เกี่ยวข้องกับการเรียกซ้อนหรือการวนซ้ำหลายครั้งไปยังสิ่งก่อสร้างที่อิงตามช่วง ในทางตรงกันข้ามการใช้ช่วงปิดสองเท่าจะทำให้เกิดโค้ดผิดพลาดและรหัสที่ไม่พึงประสงค์และมีเสียงดังมาก ตัวอย่างเช่นพิจารณาพาร์ติชัน [ n 0 , n 1 ) [ n 1 , n 2 ) [ n 2 , n 3 ) อีกตัวอย่างคือลูปการวนซ้ำมาตรฐานfor (it = begin; it != end; ++it)ซึ่งใช้end - beginเวลา รหัสที่เกี่ยวข้องจะอ่านได้น้อยลงมากหากทั้งสองรวมอยู่ด้วยและลองจินตนาการว่าคุณจะจัดการกับช่วงที่ว่างได้อย่างไร

ในที่สุดเรายังสามารถโต้แย้งได้ดีว่าทำไมการนับควรเริ่มต้นที่ศูนย์: ด้วยการประชุมครึ่งเปิดสำหรับช่วงที่เราเพิ่งจัดตั้งขึ้นถ้าคุณได้รับช่วงขององค์ประกอบN (พูดเพื่อแจกแจงสมาชิกของอาร์เรย์) จากนั้น 0 คือ "การเริ่มต้น" ตามธรรมชาติเพื่อให้คุณสามารถเขียนช่วงเป็น [0, N ) โดยไม่มีการชดเชยหรือการแก้ไขที่น่าอึดอัดใจ

โดยสรุป: ความจริงที่ว่าเราไม่เห็นจำนวน1ทุกที่ในอัลกอริทึมตามช่วงนั้นเป็นผลโดยตรงของและแรงจูงใจสำหรับการประชุม [เริ่มต้นสิ้น]


2
C ทั่วไปสำหรับวนซ้ำวนซ้ำขนาดอาร์เรย์ N คือ "สำหรับ (i = 0; i <N; i ++) a [i] = 0;" ตอนนี้คุณไม่สามารถแสดงความคิดเห็นโดยตรงกับผู้วนซ้ำหลายคนที่เสียเวลาพยายามทำให้ <มีความหมาย แต่จะเห็นได้ชัดว่าเกือบเท่ากันที่จะพูดว่า "สำหรับ (i = 0; i! = N; i ++) ... " การแมป 0 เพื่อเริ่มต้นและ N ถึงจุดสิ้นสุดจึงสะดวก
Krazy Glew

3
@ KrazyGlew: ฉันไม่ได้ใส่ประเภทไว้ในตัวอย่างลูปของฉันโดยเจตนา หากคุณคิดว่าเป็นbeginและมีค่าและตามลำดับมันเหมาะอย่างสมบูรณ์ มันเป็นเงื่อนไขที่เป็นธรรมชาติมากกว่าแบบดั้งเดิมแต่เราไม่เคยค้นพบสิ่งนั้นจนกว่าเราจะเริ่มคิดถึงคอลเล็กชั่นทั่วไปมากขึ้น endint0N!=<
Kerrek SB

4
@ KerrekSB: ฉันยอมรับว่า "เราไม่เคยให้ความสำคัญว่า [! = ดีกว่า] จนกระทั่งเราเริ่มคิดเกี่ยวกับคอลเลกชันทั่วไปมากขึ้น" IMHO ที่เป็นหนึ่งในสิ่งที่ Stepanov สมควรได้รับเครดิตสำหรับการพูดเป็นคนที่พยายามเขียนแม่แบบไลบรารีดังกล่าวก่อนที่ STL อย่างไรก็ตามฉันจะโต้แย้งเกี่ยวกับ "! =" มีความเป็นธรรมชาติมากกว่าหรือฉันจะยืนยันว่า! = อาจมีการแนะนำข้อบกพร่องที่ <จะจับได้ คิดว่า (i = 0; i! = 100; i + = 3) ...
Krazy Glew

@ KrazyGlew: จุดสุดท้ายของคุณค่อนข้างจะไม่อยู่ในหัวข้อเนื่องจากลำดับ {0, 3, 6, ... , 99} ไม่ใช่แบบฟอร์มที่ OP สอบถาม หากคุณต้องการให้เป็นเช่นนั้นคุณควรเขียน++เทมเพลตตัววนซ้ำที่step_by<3>ไม่สามารถแก้ไขได้ซึ่งจะมีซีแมนติกที่โฆษณาไว้ในตอนแรก
Kerrek SB

@KrazyGlew แม้ว่า <บางครั้งจะซ่อนข้อผิดพลาดมันเป็นข้อผิดพลาดอยู่แล้ว ถ้าใช้คน!=เมื่อเขาควรจะใช้<แล้วมันเป็นข้อผิดพลาด โดยวิธีการที่กษัตริย์ของข้อผิดพลาดที่หาง่ายด้วยการทดสอบหน่วยหรือยืนยัน
Phil1970

80

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

   +---+---+---+---+
   | A | B | C | D |
   +---+---+---+---+
   ^               ^
   |               |
 begin            end

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

   +---+---+---+---+
   | A | B | C | D |
   +---+---+---+---+
   ^       ^       ^
   |       |       |
 begin     i      end

และคุณทันทีที่เห็นว่าช่วงขององค์ประกอบจากbeginการiมีองค์ประกอบAและBขณะที่ช่วงขององค์ประกอบจากiการendมีองค์ประกอบและC Dการยกเลิกการลงทะเบียนiจะให้องค์ประกอบที่ถูกต้องนั่นคือองค์ประกอบแรกของลำดับที่สอง

แม้แต่ "off-by-one" สำหรับตัววนซ้ำแบบย้อนกลับก็เห็นได้ชัดว่า: การย้อนลำดับนั้นให้:

   +---+---+---+---+
   | D | C | B | A |
   +---+---+---+---+
   ^       ^       ^
   |       |       |
rbegin     ri     rend
 (end)    (i)   (begin)

ฉันได้เขียนตัววนซ้ำ (ฐาน) แบบ non-reverse ที่สอดคล้องกันในวงเล็บด้านล่าง คุณเห็น iterator ย้อนกลับที่อยู่i(ซึ่งผมได้ตั้งชื่อri) ยังคงชี้ในระหว่างองค์ประกอบและB Cอย่างไรก็ตามเนื่องจากการย้อนกลับของลำดับตอนนี้องค์ประกอบBจะอยู่ทางขวาของมัน


2
นี่คือ IMHO คำตอบที่ดีที่สุดแม้ว่าฉันคิดว่ามันอาจจะแสดงให้เห็นได้ดีขึ้นถ้า iterators ชี้ไปที่ตัวเลขและองค์ประกอบอยู่ระหว่างตัวเลข (ไวยากรณ์foo[i]) เป็นชวเลขสำหรับรายการทันทีหลังจากตำแหน่งi) เมื่อนึกถึงมันฉันสงสัยว่ามันอาจจะมีประโยชน์สำหรับภาษาที่จะต้องแยกโอเปอเรเตอร์สำหรับ "รายการทันทีหลังจากตำแหน่ง i" และ "รายการทันทีก่อนที่ตำแหน่งฉัน" เนื่องจากอัลกอริทึมจำนวนมากทำงานกับคู่ของรายการที่อยู่ติดกัน รายการที่ตำแหน่งทั้งสองด้านของ i "อาจสะอาดกว่า" รายการที่ตำแหน่ง i และ i + 1 "
supercat

@supercat: ตัวเลขไม่ควรระบุตำแหน่งตัวบ่งชี้ / ดัชนีซ้ำ แต่เพื่อระบุองค์ประกอบด้วยตนเอง ฉันจะแทนที่ตัวเลขด้วยตัวอักษรเพื่อให้ชัดเจนขึ้น แน่นอนด้วยตัวเลขตามที่กำหนดbegin[0](สมมติว่าเป็นตัววนซ้ำการเข้าถึงแบบสุ่ม) จะเข้าถึงองค์ประกอบ1เนื่องจากไม่มีองค์ประกอบ0ในลำดับตัวอย่างของฉัน
celtschk

เหตุใดจึงใช้คำว่า "เริ่มต้น" แทนที่จะใช้ "เริ่มต้น" หลังจากทั้งหมด "เริ่มต้น" เป็นคำกริยา
user1741137

@ user1741137 ฉันคิดว่า "เริ่มต้น" มีไว้เพื่อเป็นตัวย่อของ "เริ่มต้น" (ซึ่งตอนนี้สมเหตุสมผล) "การเริ่มต้น" ยาวเกินไป "เริ่มต้น" ฟังดูเหมือนเหมาะสมดี "เริ่มต้น" จะขัดแย้งกับคำกริยา "เริ่มต้น" (ตัวอย่างเช่นเมื่อคุณต้องกำหนดฟังก์ชั่นstart()ในชั้นเรียนของคุณสำหรับการเริ่มต้นกระบวนการที่เฉพาะเจาะจงหรืออะไรก็ตามมันจะน่ารำคาญถ้าขัดแย้งกับสิ่งที่มีอยู่แล้ว)
Fareanor

74

เหตุใดมาตรฐานจึงกำหนดend()เป็นจุดสิ้นสุดที่ผ่านมาแทนที่จะเป็นที่จุดสิ้นสุดจริง

เพราะ:

  1. มันหลีกเลี่ยงการจัดการพิเศษสำหรับช่วงที่ว่างเปล่า สำหรับช่วงที่ว่างเปล่าbegin()เท่ากับ end()&
  2. มันทำให้เกณฑ์สิ้นสุดง่าย ๆ สำหรับลูปที่วนรอบองค์ประกอบ: ลูปจะดำเนินต่อไปตราบเท่าที่end()ยังไม่ถึง

64

เพราะว่าตอนนั้น

size() == end() - begin()   // For iterators for whom subtraction is valid

และคุณจะไม่ต้องทำสิ่งที่น่าอึดอัดใจเช่นนั้น

// Never mind that this is INVALID for input iterators...
bool empty() { return begin() == end() + 1; }

และคุณจะไม่ได้ตั้งใจเขียนรหัสผิดพลาดเหมือน

bool empty() { return begin() == end() - 1; }    // a typo from the first version
                                                 // of this post
                                                 // (see, it really is confusing)

bool empty() { return end() - begin() == -1; }   // Signed/unsigned mismatch
// Plus the fact that subtracting is also invalid for many iterators

นอกจากนี้: จะเกิดอะไรขึ้นfind()หากend()ชี้ไปที่องค์ประกอบที่ถูกต้อง
คุณจริงๆต้องการอีกสมาชิกเรียกว่าinvalid()ซึ่งผลตอบแทนที่ไม่ถูกต้อง iterator ?!
ตัววนซ้ำสองตัวนั้นเจ็บปวดพออยู่แล้ว ...

Oh, และเห็นนี้โพสต์ที่เกี่ยวข้อง


นอกจากนี้:

ถ้าendก่อนหน้านี้เป็นองค์ประกอบสุดท้ายคุณจะinsert()จบชีวิตจริงอย่างไร!


2
นี่เป็นคำตอบที่ underrated อย่างมาก ตัวอย่างมีความกระชับและตรงประเด็นและคนอื่น ๆ ก็ไม่ได้พูดว่า "ยัง" และเป็นประเภทของสิ่งที่ดูเหมือนชัดเจนในการหวนกลับ แต่ตีฉันชอบเปิดเผย
underscore_d

@underscore_d: ขอบคุณ !! :)
user541686

btw, ในกรณีที่ฉันดูเหมือนว่าคนหน้าซื่อใจคดที่ไม่ยอมแพ้นั่นเป็นเพราะฉันทำไปแล้วในเดือนกรกฎาคม 2559!
underscore_d

@underscore_d: ฮ่าฮ่าฮ่าฉันไม่ได้สังเกตเลย แต่ขอบคุณ! :)
user541686

22

สำนวน iterator ของช่วงปิดครึ่ง[begin(), end())แรกเริ่มนั้นใช้เลขคณิตของตัวชี้สำหรับอาร์เรย์ธรรมดา ในโหมดการทำงานนั้นคุณจะมีฟังก์ชั่นที่ผ่านอาร์เรย์และขนาด

void func(int* array, size_t size)

การแปลงเป็นช่วงปิดครึ่ง[begin, end)นั้นง่ายมากเมื่อคุณมีข้อมูลดังกล่าว:

int* begin;
int* end = array + size;

for (int* it = begin; it < end; ++it) { ... }

หากต้องการทำงานกับช่วงปิดอย่างสมบูรณ์มันยากกว่า:

int* begin;
int* end = array + size - 1;

for (int* it = begin; it <= end; ++it) { ... }

ตั้งแต่ตัวชี้ไปยังอาร์เรย์มี iterators ใน C ++ (และไวยากรณ์ที่ถูกออกแบบมาเพื่อให้นี้) มันง่ายมากที่จะเรียกกว่าก็คือการโทรstd::find(array, array + size, some_value)std::find(array, array + size - 1, some_value)


Plus ถ้าคุณทำงานกับช่วงครึ่งปิดคุณสามารถใช้!=ประกอบการเพื่อตรวจสอบสภาพท้ายที่สุดเพราะ (หากผู้ประกอบการของคุณมีการกำหนดไว้อย่างถูกต้อง) หมายถึง<!=

for (int* it = begin; it != end; ++ it) { ... }

อย่างไรก็ตามไม่มีวิธีง่ายๆในการทำเช่นนี้กับช่วงปิดเต็ม <=คุณกำลังติดอยู่กับ

ตัววนซ้ำชนิดเดียวที่รองรับ<และ>การทำงานใน C ++ เป็นตัววนซ้ำแบบเข้าถึงได้ ถ้าคุณต้องเขียน<=ผู้ประกอบการสำหรับการเรียน iterator ทุกใน C ++ คุณจะต้องทำให้ทุก iterators ของคุณเปรียบได้อย่างเต็มที่และคุณควรที่จะเลือกน้อยลงสำหรับการสร้าง iterators สามารถน้อย (เช่น iterators สองทิศทางบนstd::listหรือ iterators การป้อนข้อมูล ที่ทำงานบนiostreams) ถ้า C ++ ใช้ช่วงปิดเต็ม


8

ด้วยการend()ชี้ไปหนึ่งครั้งสุดท้ายมันเป็นเรื่องง่ายที่จะย้ำคอลเล็กชันด้วยการวนซ้ำ:

for (iterator it = collection.begin(); it != collection.end(); it++)
{
    DoStuff(*it);
}

ด้วยการend()ชี้ไปที่องค์ประกอบสุดท้ายลูปจะซับซ้อนกว่า:

iterator it = collection.begin();
while (!collection.empty())
{
    DoStuff(*it);

    if (it == collection.end())
        break;

    it++;
}

0
  1. begin() == end()ถ้าภาชนะที่ว่างเปล่า
  2. โปรแกรมเมอร์ C ++ มีแนวโน้มที่จะใช้!=แทน<(น้อยกว่า) ในend()สภาพลูปดังนั้นการชี้ไปยังตำแหน่งที่หนึ่งนอกสิ้นจึงสะดวก
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.