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ế?
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ế?
Câu trả lời:
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 - begin
thờ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).
begin
và end
như int
s với các giá trị 0
và N
, 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.
++
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.
!=
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.
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 begin
chỉ vào đầu của chuỗi, và end
chỉ đến cuối của cùng một chuỗi. Dereferences begin
truy cập vào phần tử A
, và dereferences end
khô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
để i
chứa các phần tử A
và B
trong khi phạm vi của các phần tử từ i
để end
chứa các phần tử C
và D
. Dereferences i
cung 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ử B
và C
. Tuy nhiên do đảo ngược trình tự, bây giờ phần tử B
nằm ở bên phải của nó.
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 ".
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 0
trong chuỗi ví dụ của tôi.
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).
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ì:
begin()
bằng
end()
& end()
là không đạt được.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 .
Nếu end
trước phần tử cuối cùng, bạn sẽ ở cuối cùng như thế nào insert()
?!
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ợ <
và >
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::list
hoặ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.
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++;
}
begin() == end()
.!=
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.