Mengapa rentang iterator standar [awal, akhir) bukan [mulai, akhir]?

204

Mengapa Standar didefinisikan end()sebagai yang melewati akhir, bukan pada akhir yang sebenarnya?

Anak anjing
sumber
19
Saya menduga "karena itulah yang dikatakan standar" tidak akan memotongnya, bukan? :)
Luchian Grigore
39
@LuchianGrigore: Tentu saja tidak. Itu akan mengikis rasa hormat kami untuk (orang di belakang) standar. Kita harus berharap bahwa ada alasan untuk pilihan yang dibuat oleh standar.
Kerrek SB
4
Singkatnya, komputer tidak masuk hitungan seperti orang. Tetapi jika Anda penasaran mengapa orang tidak masuk hitungan seperti komputer, saya sarankan The Nothing is Is: A Natural History of Zero untuk melihat secara mendalam masalah yang ditemukan manusia bahwa ada angka yang kurang satu. daripada satu.
John McFarlane
8
Karena hanya ada satu cara untuk menghasilkan "yang terakhir", sering kali tidak murah karena harus nyata. Menghasilkan "Anda jatuh dari ujung tebing" selalu murah, banyak kemungkinan representasi akan dilakukan. (batal *) "ahhhhhhh" akan baik-baik saja.
Hans Passant
6
Saya melihat tanggal pertanyaan dan untuk sesaat saya pikir Anda bercanda.
Asaf

Jawaban:

286

Argumen terbaik dengan mudah adalah yang dibuat oleh Dijkstra sendiri :

  • Anda ingin ukuran rentang menjadi perbedaan sederhana end  -  mulai ;

  • termasuk batas bawah lebih "alami" ketika sekuens berubah menjadi kosong, dan juga karena alternatif ( tidak termasuk batas bawah) akan membutuhkan keberadaan nilai sentinel "satu-sebelum-awal".

Anda masih perlu menjustifikasi mengapa Anda mulai menghitung dengan nol daripada satu, tetapi itu bukan bagian dari pertanyaan Anda.

Kebijaksanaan di belakang konvensi [mulai, akhir] terbayar berkali-kali ketika Anda memiliki jenis algoritma apa pun yang berhubungan dengan beberapa panggilan bersarang atau berulang untuk konstruksi berbasis rentang, yang berantai secara alami. Sebaliknya, menggunakan rentang yang ditutup dua kali lipat akan menimbulkan kode off-by-one dan sangat tidak menyenangkan dan berisik. Sebagai contoh, perhatikan partisi [ n 0 , n 1 ) [ n 1 , n 2 ) [ n 2 , n 3 ). Contoh lain adalah loop iterasi standar for (it = begin; it != end; ++it), yang berjalan end - beginkali. Kode yang sesuai akan jauh lebih mudah dibaca jika kedua ujungnya inklusif - dan bayangkan bagaimana Anda akan menangani rentang kosong.

Akhirnya, kita juga dapat membuat argumen yang bagus mengapa penghitungan harus dimulai dari nol: Dengan konvensi setengah terbuka untuk rentang yang baru saja kita buat, jika Anda diberi rentang elemen N (katakanlah untuk menyebutkan anggota array), lalu 0 adalah "awal" alami sehingga Anda dapat menulis kisaran sebagai [0, N ), tanpa offset atau koreksi yang canggung.

Singkatnya: fakta bahwa kita tidak melihat angka 1di mana-mana dalam algoritma berbasis rentang adalah konsekuensi langsung dari, dan motivasi untuk, [awal, akhir) konvensi.

Kerrek SB
sumber
2
C khas untuk loop yang berulang pada array ukuran N adalah "untuk (i = 0; i <N; i ++) a [i] = 0;". Sekarang, Anda tidak dapat mengungkapkannya secara langsung dengan iterator - banyak orang membuang-buang waktu mencoba menjadikan <bermakna. Tetapi hampir sama jelasnya untuk mengatakan "for (i = 0; i! = N; i ++) ..." Memetakan 0 untuk memulai dan N untuk mengakhiri oleh karena itu nyaman.
Krazy Glew
3
@KrazyGlew: Saya tidak sengaja memasukkan tipe dalam contoh loop saya. Jika Anda memikirkan begindan endsebagai ints dengan nilai 0dan N, masing-masing, itu sangat cocok. Boleh dibilang, itu adalah !=kondisi yang lebih alami daripada tradisional< , tetapi kami tidak pernah menemukan itu sampai kami mulai memikirkan koleksi yang lebih umum.
Kerrek SB
4
@ GerrekSB: Saya setuju bahwa "kami tidak pernah menemukan bahwa [! = Lebih baik] sampai kami mulai memikirkan koleksi yang lebih umum." IMHO itu adalah salah satu hal yang patut dihargai oleh Stepanov untuk - berbicara sebagai seseorang yang mencoba menulis perpustakaan templat seperti itu sebelum STL. Namun, saya akan berdebat tentang "! =" Menjadi lebih alami - atau, lebih tepatnya, saya akan berdebat bahwa! = Mungkin telah memperkenalkan bug, yang <akan ditangkap. Pikirkan untuk (i = 0; i! = 100; i + = 3) ...
Krazy Glew
@KrazyGlew: Poin terakhir Anda agak di luar topik, karena urutan {0, 3, 6, ..., 99} bukan dari bentuk yang ditanyakan OP. Jika Anda menginginkannya demikian, Anda harus menulis ++template iterator -incrementable step_by<3>, yang kemudian akan memiliki semantik yang semula diiklankan.
Kerrek SB
@KrazyGlew Bahkan jika <akan menyembunyikan bug, itu adalah bug . Jika seseorang menggunakan !=kapan ia harus menggunakannya <, maka itu adalah bug. Omong-omong, raja kesalahan itu mudah ditemukan dengan pengujian unit atau pernyataan.
Phil1970
80

Sebenarnya, banyak hal yang berhubungan dengan iterator tiba-tiba lebih masuk akal jika Anda mempertimbangkan iterator tidak menunjuk pada elemen-elemen dari sekuens tetapi di antaranya , dengan dereferencing mengakses elemen berikutnya tepat untuk itu. Kemudian iterator "one past end" tiba-tiba langsung masuk akal:

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

Jelas sekali begin menunjuk ke awal urutan, dan endmenunjuk ke akhir urutan yang sama. Dereferencing beginmengakses elemen A, dan dereferencing endtidak masuk akal karena tidak ada elemen yang tepat untuk itu. Juga, menambahkan iterator idi tengah memberi

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

dan Anda langsung melihat bahwa berbagai elemen dari begin hingga imengandung elemen Adan Bsementara rentang elemen dari ihingga endmengandung elemen Cdan D. Dereferencing imemberikan elemen tepat padanya, yaitu elemen pertama dari urutan kedua.

Bahkan "off-by-one" untuk iterator terbalik tiba-tiba menjadi jelas seperti itu: Membalik urutan itu memberi:

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

Saya telah menulis iterator non-reverse (base) yang sesuai dalam tanda kurung di bawah ini. Anda lihat, iterator terbalik milik i(yang saya beri nama ri) masih menunjuk di antara elemen Bdan C. Namun karena membalik urutan, sekarang elemen Bada di sebelah kanan.

celtschk
sumber
2
Ini adalah IMHO jawaban terbaik, meskipun saya pikir itu mungkin lebih baik diilustrasikan jika iterator menunjuk angka, dan elemen berada di antara angka (sintaks foo[i]) adalah singkatan untuk item segera setelah posisi i). Berpikir tentang itu, saya bertanya-tanya apakah mungkin berguna bagi bahasa untuk memiliki operator terpisah untuk "item segera setelah posisi i" dan "item segera sebelum posisi i", karena banyak algoritma bekerja dengan pasangan item yang berdekatan, dan berkata " Item di kedua sisi posisi i "mungkin lebih bersih daripada" Item di posisi i dan i + 1 ".
supercat
@supercat: Angka-angka itu tidak seharusnya menunjukkan posisi / indeks iterator, tetapi untuk menunjukkan elemen itu sendiri. Saya akan mengganti angka dengan huruf untuk membuatnya lebih jelas. Memang, dengan angka-angka seperti yang diberikan, begin[0](dengan asumsi iterator akses acak) akan mengakses elemen 1, karena tidak ada elemen 0dalam urutan contoh saya.
celtschk
Mengapa kata "mulai" digunakan daripada "mulai"? Lagi pula, "mulai" adalah kata kerja.
user1741137
@ user1741137 Saya pikir "mulai" dimaksudkan sebagai singkatan dari "awal" (yang sekarang masuk akal). "mulai" terlalu lama, "mulai" terdengar sangat pas. "start" akan bertentangan dengan kata kerja "start" (misalnya ketika Anda harus mendefinisikan fungsi start()di kelas Anda untuk memulai proses tertentu atau apa pun, itu akan mengganggu jika bertentangan dengan yang sudah ada).
Fareanor
74

Mengapa Standar didefinisikan end()sebagai yang melewati akhir, bukan pada akhir yang sebenarnya?

Karena:

  1. Ini menghindari penanganan khusus untuk rentang kosong. Untuk rentang kosong, begin()sama dengan end()&
  2. Itu membuat kriteria akhir menjadi sederhana untuk loop yang beralih pada elemen: Loop hanya berlanjut selama end()tidak tercapai.
Alok Simpan
sumber
64

Karena itu

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

dan Anda tidak perlu melakukan hal-hal aneh seperti

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

dan Anda tidak akan secara tidak sengaja menulis kode yang salah seperti

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

Juga: Apa yang akan find()kembali jika end()diarahkan ke elemen yang valid?
Apakah Anda benar - benar ingin anggota lain dipanggil invalid()yang mengembalikan iterator yang tidak valid ?!
Dua iterator sudah cukup menyakitkan ...

Oh, dan lihat posting terkait ini .


Juga:

Jika endsebelum elemen terakhir, bagaimana Anda insert()pada akhirnya?

pengguna541686
sumber
2
Ini adalah jawaban yang sangat diremehkan. Contoh-contohnya ringkas dan langsung pada intinya, dan "Juga" tidak dikatakan oleh orang lain dan merupakan hal-hal yang tampak sangat jelas dalam retrospeksi tetapi memukul saya seperti wahyu.
underscore_d
@underscore_d: Terima kasih !! :)
user541686
btw, kalau-kalau aku tampak munafik karena tidak mendukung, itu karena aku sudah melakukannya pada bulan Juli 2016!
underscore_d
@underscore_d: hahaha Aku bahkan tidak menyadarinya, tapi terima kasih! :)
user541686
22

Ungkapan iterator dari rentang setengah tertutup pada [begin(), end())awalnya didasarkan pada aritmatika pointer untuk array polos. Dalam mode operasi itu, Anda akan memiliki fungsi yang melewati array dan ukuran.

void func(int* array, size_t size)

Konversi ke rentang setengah tertutup [begin, end)sangat sederhana ketika Anda memiliki informasi itu:

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

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

Untuk bekerja dengan rentang yang tertutup sepenuhnya, lebih sulit:

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

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

Karena pointer ke array adalah iterator di C ++ (dan sintaks dirancang untuk memungkinkan ini), itu jauh lebih mudah untuk dipanggil std::find(array, array + size, some_value)daripada memanggil std::find(array, array + size - 1, some_value).


Plus, jika Anda bekerja dengan rentang setengah tertutup, Anda dapat menggunakan !=operator untuk memeriksa kondisi akhir, karena (jika operator Anda didefinisikan dengan benar) <menyiratkan !=.

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

Namun tidak ada cara mudah untuk melakukan ini dengan rentang tertutup sepenuhnya. Anda terjebak dengan<= .

Satu-satunya jenis iterator yang mendukung <dan >beroperasi di C ++ adalah iterator akses-acak. Jika Anda harus menulis <=operator untuk setiap kelas iterator di C ++, Anda harus membuat semua iterator Anda sepenuhnya dapat dibandingkan, dan Anda akan lebih sedikit pilihan untuk membuat iterator yang kurang mampu (seperti iterator dua arah aktif std::list, atau iterator input yang beroperasi pada iostreams) jika C ++ menggunakan rentang tertutup sepenuhnya.

Ken Bloom
sumber
8

Dengan end()penunjuk yang melewati akhir, mudah untuk mengulang koleksi dengan for for:

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

Dengan end()menunjuk ke elemen terakhir, sebuah loop akan menjadi lebih kompleks:

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

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

    it++;
}
Anders Abel
sumber
0
  1. Jika wadah kosong begin() == end(),.
  2. C ++ Programmer cenderung menggunakan !=alih-alih <(kurang dari) dalam kondisi loop, oleh karena itu end()menunjuk ke posisi yang tidak nyaman adalah mudah.
Andreas DM
sumber