Bài 11.3: Vòng lặp range-based for trong C++

Chào mừng các bạn quay trở lại với series học C++! Sau khi đã làm quen với các cấu trúc điều khiển cơ bản như if, else, switch, và các vòng lặp truyền thống như for, while, do-while, hôm nay chúng ta sẽ khám phá một người bạn mới, hiện đại hơn, giúp việc duyệt qua các "collection" trở nên dễ dàng và an toàn hơn rất nhiều: vòng lặp range-based for.

Được giới thiệu từ chuẩn C++11, vòng lặp range-based for (hay còn gọi là for-each loop ở một số ngôn ngữ khác) là một cú pháp tiện lợi để lặp qua tất cả các phần tử trong một range (một phạm vi). Nó giúp code của chúng ta trở nên gọn gàng, dễ đọc và quan trọng nhất là giảm thiểu các lỗi thường gặp khi làm việc với chỉ số (index), ví dụ như lỗi "off-by-one".

Hãy cùng đi sâu tìm hiểu về cú pháp và cách sử dụng tuyệt vời này nhé!

Cú pháp Cơ bản

Cú pháp của vòng lặp range-based for rất đơn giản:

for (declaration : range_expression) {
    // body của vòng lặp
}

Giải thích các thành phần:

  • declaration: Đây là phần khai báo biến sẽ đại diện cho mỗi phần tử trong range_expression ở mỗi lần lặp. Kiểu dữ liệu của biến này thường được suy luận tự động (auto) hoặc bạn có thể chỉ định rõ ràng. Quan trọng là bạn cần quyết định xem bạn muốn làm việc với bản sao của phần tử hay làm việc trực tiếp với tham chiếu đến phần tử gốc (có thể là tham chiếu hằng const& hoặc tham chiếu có thể thay đổi &). Chúng ta sẽ nói kỹ hơn về điều này sau.
  • range_expression: Đây là "collection" mà bạn muốn lặp qua. Nó có thể là:
    • Một mảng C-style (như int arr[] = {1, 2, 3};).
    • Một đối tượng của các container trong Thư viện Chuẩn C++ (STL) như vector, list, string, map, set, v.v., miễn là chúng hỗ trợ các hàm begin()end() (hoặc cbegin()cend() cho hằng).
    • Bất kỳ kiểu dữ liệu nào mà bạn tự định nghĩa và triển khai giao diện iterator (cung cấp begin()end()).
  • body của vòng lặp: Khối code sẽ được thực thi cho mỗi phần tử trong range_expression.

Cách Thức Hoạt Động (Tổng quan)

Về cơ bản, trình biên dịch sẽ tự động "dịch" vòng lặp range-based for của bạn sang một vòng lặp sử dụng iterator (đối với container STL) hoặc chỉ số (đối với mảng C-style) một cách an toàn.

Ví dụ, với một vector<int> v, vòng lặp:

for (int x : v) {
    // ...
}

Sẽ tương đương (một cách đơn giản hóa) với:

for (auto _it = v.begin(); _it != v.end(); ++_it) {
    int x = *_it; // x là bản sao của phần tử
    // ...
}

Còn với một mảng C-style int arr[], nó sẽ tương đương với:

int arr[] = {1, 2, 3};
for (size_t i = 0; i < sizeof(arr)/sizeof(arr[0]); ++i) {
    int x = arr[i]; // x là bản sao của phần tử
    // ...
}

Việc hiểu cách nó hoạt động ngầm giúp bạn biết khi nào nên sử dụng range-based for và khi nào nên dùng vòng lặp truyền thống.

Duyệt qua các Range Khác Nhau với Range-based for

Hãy xem các ví dụ minh họa cụ thể với các loại "range" phổ biến.

1. Duyệt Mảng C-style

Đây là cách đơn giản nhất để sử dụng range-based for.

#include <iostream>

int main() {
    int soNguyen[] = {10, 20, 30, 40, 50};

    cout << "**Duyệt mảng soNguyen:**" << endl;
    for (int phanTu : soNguyen) {
        cout << phanTu << " ";
    }
    cout << endl; // Xuống dòng sau khi duyệt xong

    return 0;
}

Giải thích: soNguyen là mảng C-style. Biến phanTu sẽ lần lượt nhận giá trị của từng phần tử trong mảng ở mỗi lần lặp. Ở đây, chúng ta sử dụng kiểu int phanTu, nghĩa là mỗi lần lặp, một bản sao của phần tử trong mảng sẽ được tạo và gán cho phanTu.

2. Duyệt vector

vector là một trong những container phổ biến nhất trong C++ STL. Range-based for rất phù hợp để duyệt vector.

#include <iostream>
#include <vector>
#include <string>

int main() {
    vector<string> danhSachTen = {"Alice", "Bob", "Charlie", "David"};

    cout << "**Duyệt vector danhSachTen:**" << endl;
    for (const string& ten : danhSachTen) { // Nên dùng const& cho kiểu dữ liệu phức tạp
        cout << "Ten: " << ten << endl;
    }

    return 0;
}

Giải thích: danhSachTen là một vector chứa các chuỗi (string). Chúng ta sử dụng const string& ten làm declaration. Tại sao lại dùng const&?

  • Sử dụng & (tham chiếu) giúp tránh việc tạo ra một bản sao tạm thời của mỗi chuỗi trong vector, điều này hiệu quả hơn về mặt bộ nhớ và tốc độ khi làm việc với các đối tượng lớn hoặc phức tạp như string.
  • Sử dụng const đảm bảo rằng chúng ta không thể vô tình thay đổi giá trị của các chuỗi trong vector thông qua biến ten trong vòng lặp. Đây là cách tốt nhất để duyệt các container khi bạn chỉ muốn đọc dữ liệu.
3. Duyệt string

Một string về bản chất là một chuỗi các ký tự, và nó cũng hoạt động như một "range".

#include <iostream>
#include <string>

int main() {
    string thongDiep = "Xin chao C++!";

    cout << "**Duyệt string thongDiep:**" << endl;
    for (char kyTu : thongDiep) {
        cout << kyTu << "-";
    }
    cout << endl;

    return 0;
}

Giải thích: thongDiep là một string. Biến kyTu sẽ lần lượt là từng ký tự trong chuỗi. Vì char là kiểu dữ liệu nhỏ và đơn giản, việc tạo bản sao (char kyTu) thường không gây tốn kém và đơn giản hơn so với dùng tham chiếu. Tuy nhiên, bạn vẫn có thể dùng const char& kyTu nếu muốn.

4. Duyệt các Container Khác (list, set, map,...)

Vòng lặp range-based for hoạt động với hầu hết các container chuẩn trong STL.

#include <iostream>
#include <list>
#include <set>
#include <map>
#include <string>

int main() {
    // Duyệt list
    list<double> diemSo = {8.5, 7.0, 9.2};
    cout << "**Duyệt list diemSo:**" << endl;
    for (double diem : diemSo) { // Dùng copy cũng được cho double
        cout << diem << " ";
    }
    cout << endl;

    // Duyệt set
    set<int> tapHopSo = {10, 5, 20, 5}; // Lưu ý: set chỉ lưu các giá trị duy nhất, sắp xếp
    cout << "**Duyệt set tapHopSo:**" << endl;
    for (int so : tapHopSo) {
        cout << so << " ";
    }
    cout << endl;

    // Duyệt map
    map<string, int> tuoiNguoi;
    tuoiNguoi["Nam"] = 25;
    tuoiNguoi["Lan"] = 30;
    tuoiNguoi["Anh"] = 28;

    cout << "**Duyệt map tuoiNguoi:**" << endl;
    // map lưu trữ các cặp key-value dưới dạng pair<const Key, Value>
    // Từ C++17, có thể dùng structured binding để dễ dàng truy cập key và value
    for (const auto& [ten, tuoi] : tuoiNguoi) { // Structured binding (C++17)
        cout << ten << " is " << tuoi << " years old." << endl;
    }
    // Nếu không dùng C++17 structured binding, bạn sẽ duyệt qua các pair:
    // for (const auto& pair : tuoiNguoi) {
    //     cout << pair.first << " is " << pair.second << " years old." << endl;
    // }


    return 0;
}

Giải thích: Các ví dụ trên cho thấy range-based for hoạt động nhất quán trên nhiều loại container khác nhau, làm cho code của bạn đồng nhất và dễ hiểu hơn. Đối với map, mỗi phần tử là một pair. C++17 giới thiệu structured binding ([ten, tuoi]) giúp bạn "giải nén" cặp pair thành các biến riêng lẻ ngay trong khai báo vòng lặp, cực kỳ tiện lợi!

Làm việc với Tham chiếu (&) để Thay đổi Phần tử Gốc

Như đã nói ở trên, bạn có thể sử dụng tham chiếu trong phần declaration để trực tiếp làm việc và thay đổi các phần tử ngay trong container gốc. Điều này rất hữu ích khi bạn muốn cập nhật giá trị của các phần tử.

#include <iostream>
#include <vector>

int main() {
    vector<int> soCanGapDoi = {1, 2, 3, 4, 5};

    cout << "**Vector gốc:**" << endl;
    for (int so : soCanGapDoi) {
        cout << so << " ";
    }
    cout << endl;

    cout << "**Gấp đôi các phần tử sử dụng reference:**" << endl;
    for (int& so : soCanGapDoi) { // Sử dụng tham chiếu & để sửa đổi gốc
        so = so * 2; // Thay đổi trực tiếp phần tử trong vector
    }

    cout << "**Vector sau khi gấp đôi:**" << endl;
    for (int so : soCanGapDoi) {
        cout << so << " ";
    }
    cout << endl;

    return 0;
}

Giải thích: Bằng cách sử dụng int& so trong vòng lặp, mỗi lần gán so = so * 2; sẽ thay đổi trực tiếp giá trị của phần tử tương ứng trong vector soCanGapDoi. Kết quả là vector gốc bị thay đổi sau vòng lặp.

Lợi ích của Vòng lặp Range-based for

  • Độ đọc: Code trở nên gọn gàng và dễ đọc hơn rất nhiều. Ý định "duyệt qua tất cả các phần tử" được thể hiện rõ ràng.
  • An toàn: Giảm thiểu đáng kể các lỗi liên quan đến chỉ số (ví dụ: lặp sai giới hạn, lỗi off-by-one), vốn rất phổ biến với vòng lặp for truyền thống khi duyệt mảng hoặc container.
  • Ngắn gọn: Thường yêu cầu ít dòng code hơn so với vòng lặp truyền thống.
  • Hiệu quả: Với các container STL, nó hoạt động dựa trên iterator, vốn là cách hiệu quả để duyệt các cấu trúc dữ liệu này.

Khi nào Không nên sử dụng Range-based for?

Mặc dù rất tiện lợi, range-based for không thay thế hoàn toàn các vòng lặp truyền thống. Có những trường hợp bạn cần sử dụng vòng lặp for truyền thống hoặc while:

  • Khi bạn cần chỉ số (index): Range-based for không cung cấp chỉ số của phần tử hiện tại trong vòng lặp. Nếu bạn cần chỉ số để thực hiện logic nào đó (ví dụ: truy cập một mảng song song, kiểm tra vị trí, v.v.), bạn sẽ cần vòng lặp for truyền thống.
  • Khi bạn cần lặp ngược: Range-based for luôn lặp từ đầu đến cuối range. Để lặp ngược, bạn cần sử dụng iterator ngược (rbegin(), rend()) với vòng lặp for truyền thống hoặc các kỹ thuật khác.
  • Khi bạn cần bỏ qua phần tử hoặc nhảy bước: Nếu bạn chỉ muốn xử lý các phần tử lẻ, chẵn, hoặc nhảy qua N phần tử một lúc, vòng lặp for truyền thống với bước nhảy tùy chỉnh sẽ phù hợp hơn.
  • Khi bạn cần sửa đổi cấu trúc của container trong khi đang lặp: Ví dụ, thêm hoặc xóa phần tử khỏi vector hoặc list trong khi đang duyệt bằng range-based for có thể dẫn đến hành vi không xác định (invalidating iterators). Trong trường hợp này, bạn cần sử dụng vòng lặp dựa trên iterator truyền thống và cẩn thận xử lý giá trị trả về của các hàm xóa để cập nhật iterator.
  • Khi bạn cần điều khiển vòng lặp phức tạp: Break sớm dựa trên điều kiện không liên quan trực tiếp đến phần tử hiện tại, hoặc cần một logic dừng vòng lặp đặc biệt.

Tóm lại

Vòng lặp range-based for là một công cụ mạnh mẽ và tiện lợi được bổ sung vào C++ từ chuẩn C++11. Nó giúp việc duyệt qua các mảng và container trở nên dễ dàng, an toàn và dễ đọc hơn. Hãy ưu tiên sử dụng range-based for bất cứ khi nào bạn chỉ cần lặp qua tất cả các phần tử của một range theo thứ tự tự nhiên và không cần chỉ số. Tuy nhiên, hãy nhớ các trường hợp mà vòng lặp truyền thống vẫn là lựa chọn tốt hơn.

Việc nắm vững và sử dụng thành thạo range-based for sẽ giúp code C++ của bạn hiện đại hơn và ít gặp lỗi hơn. Hãy thực hành với các ví dụ trên và áp dụng nó vào các bài tập của bạn nhé!

Comments

There are no comments at the moment.