Tại sao các phạm vi lặp tiêu chuẩn [bắt đầu, kết thúc) thay vì [bắt đầu, kết thúc]?


204

Tại sao Tiêu chuẩn lại xác định end()là một trong quá khứ, thay vì ở cuối thực tế?


19
Tôi đoán "bởi vì đó là những gì tiêu chuẩn nói" sẽ không cắt nó, phải không? :)
Luchian Grigore

39
@LuchianGrigore: Tất nhiên là không. Điều đó sẽ làm xói mòn sự tôn trọng của chúng tôi đối với (những người đứng sau) tiêu chuẩn. Chúng ta nên hy vọng rằng có một lý do cho các lựa chọn theo tiêu chuẩn.
Kerrek SB

4
Nói tóm lại, máy tính không được tính như con người. Nhưng nếu bạn tò mò về lý do tại sao mọi người không tính như máy tính, tôi khuyên bạn nên Không có gì: Lịch sử tự nhiên của Zero để có cái nhìn sâu sắc về những rắc rối mà con người đã khám phá ra rằng có một con số ít hơn một hơn một.
John McFarlane

8
Bởi vì chỉ có một cách để tạo ra "cái cuối cùng", nó thường không rẻ vì nó phải có thật. Tạo ra "bạn rơi xuống cuối vách đá" luôn rẻ, nhiều đại diện có thể sẽ làm. (void *) "ahhhhhhh" sẽ làm tốt.
Hans Passant

6
Tôi nhìn vào ngày của câu hỏi và trong một giây, tôi nghĩ bạn đang đùa.
Asaf

Câu trả lời:


286

Cuộc tranh luận tốt nhất dễ dàng là cuộc tranh luận do chính Dijkstra đưa ra :

  • Bạn muốn kích thước của phạm vi là một kết thúc khác biệt đơn giản  -  bắt đầu ;

  • bao gồm cả giới hạn dưới là "tự nhiên" hơn khi các chuỗi suy biến thành các khoảng trống và cũng bởi vì sự thay thế ( không bao gồm giới hạn dưới) sẽ yêu cầu sự tồn tại của giá trị trọng tâm "trước khi bắt đầu".

Bạn vẫn cần phải giải thích lý do tại sao bạn bắt đầu đếm bằng 0 chứ không phải một, nhưng đó không phải là một phần câu hỏi của bạn.

Sự khôn ngoan đằng sau quy ước [bắt đầu, kết thúc) trả hết thời gian và một lần nữa khi bạn có bất kỳ loại thuật toán nào liên quan đến nhiều cuộc gọi lồng nhau hoặc lặp đi lặp lại cho các cấu trúc dựa trên phạm vi, chuỗi tự nhiên. Ngược lại, sử dụng một phạm vi gấp đôi sẽ phát sinh từ bên ngoài và mã cực kỳ khó chịu và ồn ào. Ví dụ, hãy xem xét một phân vùng [ n 0 , n 1 ) [ n 1 , n 2 ) [ n 2 , n 3 ). Một ví dụ khác là vòng lặp tiêu chuẩn for (it = begin; it != end; ++it), chạy end - beginthời gian. Mã tương ứng sẽ dễ đọc hơn nhiều nếu cả hai đầu được bao gồm - và tưởng tượng cách bạn xử lý các phạm vi trống.

Cuối cùng, chúng ta cũng có thể đưa ra một lập luận hay tại sao việc đếm bắt đầu từ 0: Với quy ước nửa mở cho các phạm vi mà chúng ta vừa thiết lập, nếu bạn được cung cấp một phạm vi N phần tử (nói để liệt kê các thành viên của một mảng), thì 0 là "bắt đầu" tự nhiên để bạn có thể viết phạm vi dưới dạng [0, N ), mà không có bất kỳ sự bù đắp hay chỉnh sửa khó xử nào.

Tóm lại: việc chúng ta không thấy số 1ở mọi nơi trong các thuật toán dựa trên phạm vi là hệ quả trực tiếp và động lực cho quy ước [bắt đầu, kết thúc).


2
C điển hình cho vòng lặp lặp trên một mảng có kích thước N là "for (i = 0; i <N; i ++) a [i] = 0;". Bây giờ, bạn không thể diễn đạt điều đó trực tiếp với các trình vòng lặp - nhiều người đã lãng phí thời gian để cố gắng làm cho <có ý nghĩa. Nhưng việc nói "for (i = 0; i! = N; i ++) ..." Việc ánh xạ 0 để bắt đầu và do đó kết thúc là thuận tiện.
Krazy Glew

3
@KrazyGlew: Tôi đã không cố tình đưa các loại vào ví dụ vòng lặp của mình. Nếu bạn nghĩ beginendnhư ints với các giá trị 0N, tương ứng, nó phù hợp hoàn hảo. Có thể cho rằng, đó là !=điều kiện tự nhiên hơn truyền thống <, nhưng chúng tôi chưa bao giờ phát hiện ra điều đó cho đến khi chúng tôi bắt đầu nghĩ về những bộ sưu tập tổng quát hơn.
Kerrek SB

4
@KerrekSB: Tôi đồng ý rằng "chúng tôi chưa bao giờ phát hiện ra rằng [! = Tốt hơn] cho đến khi chúng tôi bắt đầu nghĩ về những bộ sưu tập tổng quát hơn." IMHO đó là một trong những điều mà Stepanov xứng đáng được ghi nhận - nói như một người đã cố gắng viết các thư viện mẫu như vậy trước STL. Tuy nhiên, tôi sẽ tranh luận về việc "! =" Trở nên tự nhiên hơn - hay nói đúng hơn là tôi sẽ tranh luận rằng! = Có lẽ đã giới thiệu các lỗi, mà <sẽ bắt được. Hãy suy nghĩ cho (i = 0; i! = 100; i + = 3) ...
Krazy Glew

@KrazyGlew: Điểm cuối cùng của bạn hơi lạc đề, vì chuỗi {0, 3, 6, ..., 99} không phải là dạng mà OP đã hỏi. Nếu bạn muốn nó như vậy, bạn nên viết một ++mẫu lặp lặp step_by<3>có thể điều chỉnh được, sau đó sẽ có ngữ nghĩa được quảng cáo ban đầu.
Kerrek SB

@KrazyGlew Ngay cả khi <đôi khi sẽ che giấu một lỗi, dù sao đó cũng là một lỗi . Nếu ai đó sử dụng !=khi anh ta nên sử dụng <, thì đó là một lỗi. Nhân tiện, vua lỗi đó rất dễ tìm thấy với kiểm tra đơn vị hoặc xác nhận.
Phil1970

80

Trên thực tế, rất nhiều thứ liên quan đến trình vòng lặp đột nhiên có ý nghĩa hơn nhiều nếu bạn xem xét các trình vòng lặp không chỉ vào các phần tử của chuỗi mà ở giữa , với hội nghị truy cập vào phần tử tiếp theo ngay với nó. Sau đó, trình lặp "một quá khứ" đột nhiên có ý nghĩa ngay lập tức:

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

Rõ ràng beginchỉ vào đầu của chuỗi, và endchỉ đến cuối của cùng một chuỗi. Dereferences begintruy cập vào phần tử A, và dereferences endkhông có ý nghĩa bởi vì không có phần tử nào đúng với nó. Ngoài ra, thêm một iterator iở giữa cho

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

và bạn ngay lập tức thấy rằng phạm vi của các phần tử từ beginđể ichứa các phần tử ABtrong khi phạm vi của các phần tử từ iđể endchứa các phần tử CD. Dereferences icung cấp cho phần tử bên phải của nó, đó là phần tử đầu tiên của chuỗi thứ hai.

Ngay cả "off-by-one" cho các trình vòng lặp ngược cũng đột nhiên trở nên rõ ràng theo cách đó: Đảo ngược chuỗi đó mang lại:

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

Tôi đã viết các trình lặp không đảo ngược (cơ sở) tương ứng trong ngoặc đơn bên dưới. Bạn thấy đấy, trình lặp ngược thuộc về i(mà tôi đã đặt tên ri) vẫn chỉ ở giữa các phần tử BC. Tuy nhiên do đảo ngược trình tự, bây giờ phần tử Bnằm ở bên phải của nó.


2
Đây là IMHO câu trả lời tốt nhất, mặc dù tôi nghĩ nó có thể được minh họa tốt hơn nếu các trình lặp chỉ vào các số và các phần tử nằm giữa các số (cú pháp foo[i]) là một tốc ký cho mục ngay sau vị trí i). Nghĩ về nó, tôi tự hỏi liệu một ngôn ngữ có thể có các toán tử riêng biệt cho "mục ngay sau vị trí i" và "mục ngay trước vị trí i" hay không, vì rất nhiều thuật toán hoạt động với các cặp mục liền kề và nói " Các mục ở hai bên của vị trí i "có thể sạch hơn" Các mục ở vị trí i và i + 1 ".
supercat

@supercat: Các số không được cho là chỉ ra các vị trí / chỉ số lặp, nhưng để chỉ ra các phần tử. Tôi sẽ thay thế các số bằng chữ cái để làm cho rõ ràng hơn. Thật vậy, với các số như đã cho, begin[0](giả sử một trình vòng lặp truy cập ngẫu nhiên) sẽ truy cập phần tử 1, vì không có phần tử nào 0trong chuỗi ví dụ của tôi.
celtschk

Tại sao từ "bắt đầu" được sử dụng thay vì "bắt đầu"? Rốt cuộc, "bắt đầu" là một động từ.
dùng1741137

@ user1741137 Tôi nghĩ "bắt đầu" có nghĩa là viết tắt của "bắt đầu" (mà bây giờ có ý nghĩa). "Bắt đầu" quá dài, "bắt đầu" nghe có vẻ phù hợp. "start" sẽ mâu thuẫn với động từ "start" (ví dụ khi bạn phải xác định một hàm start()trong lớp để bắt đầu một quy trình cụ thể hoặc bất cứ điều gì, sẽ rất khó chịu nếu nó xung đột với một động từ đã tồn tại).
Fareanor

74

Tại sao Tiêu chuẩn lại xác định end()là một trong quá khứ, thay vì ở cuối thực tế?

Bởi vì:

  1. Nó tránh xử lý đặc biệt cho phạm vi trống. Đối với phạm vi trống, begin()bằng end()&
  2. Nó làm cho tiêu chí kết thúc trở nên đơn giản đối với các vòng lặp lặp qua các phần tử: Các vòng lặp chỉ đơn giản là tiếp tục miễn end()là không đạt được.

64

Bởi vì lúc đó

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

và bạn sẽ không phải làm những việc khó xử như

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

và bạn sẽ không vô tình viết mã sai như

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

Ngoài ra: Điều gì sẽ find()trở lại nếu được end()chỉ ra một yếu tố hợp lệ?
Bạn có thực sự muốn một thành viên khác được gọi invalid()trả về một trình vòng lặp không hợp lệ không?!
Hai vòng lặp đã đủ đau ...

Oh, và xem bài viết liên quan này .


Cũng thế:

Nếu endtrước phần tử cuối cùng, bạn sẽ cuối cùng như thế nào insert()?!


2
Đây là một câu trả lời được đánh giá thấp. Các ví dụ ngắn gọn và đi thẳng vào vấn đề, và "Người khác" cũng không được nói bởi bất kỳ ai khác và là những điều có vẻ rất rõ ràng khi nhìn lại nhưng đánh vào tôi như những điều mặc khải.
gạch dưới

@underscore_d: Cảm ơn bạn !! :)
dùng541686

btw, trong trường hợp tôi có vẻ như là một kẻ đạo đức giả vì đã không nâng cao, đó là vì tôi đã quay trở lại vào tháng 7 năm 2016!
gạch dưới

@underscore_d: hahaha Tôi thậm chí không nhận thấy, nhưng cảm ơn! :)
dùng541686

22

Thành ngữ lặp của các phạm vi nửa đóng [begin(), end())ban đầu được dựa trên số học con trỏ cho các mảng đơn giản. Trong chế độ hoạt động đó, bạn sẽ có các hàm được truyền qua một mảng và kích thước.

void func(int* array, size_t size)

Chuyển đổi sang phạm vi nửa kín [begin, end)rất đơn giản khi bạn có thông tin đó:

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

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

Để làm việc với phạm vi đóng hoàn toàn, khó hơn:

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

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

Vì các con trỏ tới mảng là các trình lặp trong C ++ (và cú pháp được thiết kế để cho phép điều này), nên việc gọi dễ dàng std::find(array, array + size, some_value)hơn nhiều so với gọi std::find(array, array + size - 1, some_value).


Ngoài ra, nếu bạn làm việc với các phạm vi nửa kín, bạn có thể sử dụng !=toán tử để kiểm tra điều kiện kết thúc, vì becuase (nếu toán tử của bạn được xác định chính xác) <ngụ ý !=.

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

Tuy nhiên, không có cách dễ dàng để làm điều này với phạm vi đóng hoàn toàn. Bạn đang bị mắc kẹt với <=.

Loại trình vòng lặp duy nhất hỗ trợ <>hoạt động trong C ++ là các trình vòng lặp truy cập ngẫu nhiên. Nếu bạn phải viết một <=toán tử cho mọi lớp trình vòng lặp trong C ++, bạn sẽ phải làm cho tất cả các trình vòng lặp của mình có thể so sánh hoàn toàn và bạn sẽ có ít lựa chọn hơn để tạo các trình vòng lặp có khả năng kém hơn (như trình vòng lặp hai chiều trên std::listhoặc trình vòng lặp đầu vào hoạt động trên iostreams) nếu C ++ sử dụng phạm vi đóng hoàn toàn.


8

Với việc end()chỉ một điểm qua cuối, thật dễ dàng để lặp lại một bộ sưu tập với một vòng lặp for:

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

Với việc end()chỉ đến phần tử cuối cùng, một vòng lặp sẽ phức tạp hơn:

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

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

    it++;
}

0
  1. Nếu một container rỗng , begin() == end().
  2. Các lập trình viên C ++ có xu hướng sử dụng !=thay vì <(ít hơn) trong các điều kiện vòng lặp, do đó việc end()chỉ đến một vị trí một đầu cuối là thuận tiện.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.