"Với mỗi phần tử, làm việc này"
Vòng lặp for đếm kiểu truyền thống rất hợp khi bạn cần một chỉ số để điều khiển. Nhưng phần lớn thời gian bạn không thật sự quan tâm đến chỉ số, bạn chỉ muốn xử lý từng phần tử của một container. Viết for (int i = 0; i < v.size(); i++) cho việc đó vừa rườm rà, vừa khiến i chỉ cách một lỗi lệch một đơn vị là đọc ra ngoài phạm vi.
C++11 thêm vòng lặp for dạng range (range-based for loop) cho đúng mục đích này. Bạn đặt tên một biến, chỉ ra container, và vòng lặp tự đi qua từng phần tử:
Không chỉ số, không .size(), không giới hạn nào để viết sai. Hãy đọc nó là "với mỗi s trong scores". Nó dùng được với mảng thô, std::vector, std::string, std::map và mọi thứ có begin() và end().
Để auto chọn kiểu
Viết rõ kiểu phần tử vẫn chạy, nhưng dễ vỡ: đổi kiểu của container là phải sửa mọi vòng lặp. Kết hợp for dạng range với auto, trình biên dịch sẽ tự suy ra kiểu phần tử cho bạn:
Tuy nhiên ở đây có một chi phí ẩn. auto name thông thường suy ra string và sao chép từng phần tử vào name ở mỗi lần lặp. Với int thì không tốn gì; với string hay một struct lớn thì mỗi lần lặp lại tốn một lần cấp phát vô ích. Cách khắc phục là tham chiếu, và đó là điều tiếp theo cần hiểu.
Sửa trực tiếp với auto&
Nếu viết auto x, bạn nhận được một bản sao, nên gán vào x chỉ thay đổi bản sao chứ không phải container. Xem cái bẫy này:
Phép nhân âm thầm chẳng có tác dụng gì vì n chỉ là một bản sao dùng xong bỏ. Muốn thật sự sửa các phần tử, hãy lấy chúng theo tham chiếu với auto&:
Chỉ một dấu & là toàn bộ khác biệt giữa "chỉ xem không sửa" và "sửa trực tiếp". Nếu bạn từng thắc mắc vì sao thay đổi của mình biến mất, gần như chắc chắn đây là lý do.
Đọc mà không sao chép: const auto&
Khi bạn chỉ cần đọc phần tử nhưng chúng tốn kém để sao chép, hãy dùng const auto&. Tham chiếu giúp tránh sao chép, còn const thể hiện (và bắt buộc) rằng bạn sẽ không sửa gì:
Một quy tắc dễ nhớ:
for (auto x : c) // copy - cheap types (int, char, pointers)
for (auto& x : c) // edit - you want to change the elements
for (const auto& x : c) // read - heavy types you only inspect
Mặc định dùng const auto& khi đọc và auto& khi ghi. Chỉ dùng auto thông thường cho những kiểu thật sự nhỏ, rẻ để sao chép.
Duyệt map và pair
Vòng lặp for dạng range trên một std::map trả về một std::pair cho mỗi phần tử, với .first (khóa) và .second (giá trị). Từ C++17, structured binding cho phép bạn tách pair đó thành hai biến có tên ngay trong phần đầu vòng lặp:
[name, age] rõ ràng hơn nhiều so với việc lặp lại entry.first và entry.second ở khắp nơi. Ở đây cũng nên giữ const auto&: khóa của một phần tử map là string, nên sao chép từng pair sẽ rất lãng phí.
Cái bẫy: đừng đổi kích thước khi đang lặp
Cái bẫy lớn nhất là thay đổi kích thước container trong khi vòng lặp for dạng range đang duyệt nó. Gọi push_back, erase, insert hay clear có thể cấp phát lại vùng nhớ bên dưới và làm các iterator bên trong vòng lặp mất hiệu lực. Kết quả là hành vi không xác định, nghĩa là crash hoặc dữ liệu rác, chứ không phải một thông báo lỗi dễ hiểu:
vector<int> v = {1, 2, 3};
for (int x : v) {
v.push_back(x); // UNDEFINED BEHAVIOR - reallocation invalidates the range
}
Nếu cần thêm hay xóa phần tử trong lúc xử lý, hãy chuyển sang vòng lặp for theo chỉ số hoặc theo iterator và tự quản lý giới hạn, hoặc tạo một container kết quả riêng rồi hoán đổi vào sau. Hai cái bẫy nhỏ hơn cùng họ: đừng bao giờ gắn vòng lặp for dạng range vào một đối tượng tạm bị hủy ngay lập tức (for (auto x : makeVector()) thì ổn, nhưng for (auto& x : someObj.getTempVector()) có thể bị treo), và nhớ rằng for (auto& c : myString) cho phép bạn sửa trực tiếp từng ký tự.
Tiếp theo: hàm
Vòng lặp for dạng range giúp việc duyệt gọn gàng hơn, và các lựa chọn auto / auto& / const auto& bạn vừa học sẽ áp dụng thẳng vào một trong những công cụ quan trọng nhất của C++. Tiếp theo, chúng ta sẽ đóng gói logic vào các hàm có thể dùng lại: đặt cho code một cái tên, tham số và giá trị trả về để bạn gọi nó từ bất cứ đâu thay vì lặp lại chính mình.
Câu hỏi thường gặp
Vòng lặp for dạng range trong C++ là gì?
Vòng lặp for dạng range duyệt qua mọi phần tử của một container (mảng, vector, string, map...) mà bạn không cần tự quản lý chỉ số hay iterator. Cú pháp là for (auto x : container) { ... }. Nó có từ C++11 và là cách gọn nhất để nói "làm việc này với từng phần tử".
Khi nào nên dùng auto& thay vì auto trong vòng lặp for dạng range?
Dùng auto& x khi bạn muốn sửa trực tiếp các phần tử, và const auto& x khi bạn chỉ đọc nhưng muốn tránh sao chép (quan trọng với string, vector hay các đối tượng lớn). auto x thông thường tạo một bản sao ở mỗi lần lặp: ổn với kiểu rẻ như int, nhưng lãng phí với các kiểu khác.
Có thể thay đổi kích thước vector trong vòng lặp for dạng range không?
Không. Gọi push_back, erase, insert hay clear trên container đang được duyệt sẽ làm các iterator bên trong vòng lặp mất hiệu lực và là hành vi không xác định: chương trình có thể crash hoặc âm thầm làm hỏng dữ liệu. Nếu cần thêm hay xóa phần tử trong lúc lặp, hãy dùng vòng lặp for theo chỉ số hoặc theo iterator.