Chúng ta hãy bắt đầu phân biệt giữa việc quan sát các yếu tố trong thùng chứa so với sửa đổi chúng tại chỗ.
Quan sát các yếu tố
Hãy xem xét một ví dụ đơn giản:
vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)
cout << x << ' ';
Đoạn mã trên in các phần tử int
trong vector
:
1 3 5 7 9
Bây giờ hãy xem xét một trường hợp khác, trong đó các phần tử vectơ không chỉ là các số nguyên đơn giản, mà là các thể hiện của một lớp phức tạp hơn, với hàm tạo sao chép tùy chỉnh, v.v.
// A sample test class, with custom copy semantics.
class X
{
public:
X()
: m_data(0)
{}
X(int data)
: m_data(data)
{}
~X()
{}
X(const X& other)
: m_data(other.m_data)
{ cout << "X copy ctor.\n"; }
X& operator=(const X& other)
{
m_data = other.m_data;
cout << "X copy assign.\n";
return *this;
}
int Get() const
{
return m_data;
}
private:
int m_data;
};
ostream& operator<<(ostream& os, const X& x)
{
os << x.Get();
return os;
}
Nếu chúng ta sử dụng for (auto x : v) {...}
cú pháp trên với lớp mới này:
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (auto x : v)
{
cout << x << ' ';
}
đầu ra giống như:
[... copy constructor calls for vector<X> initialization ...]
Elements:
X copy ctor.
1 X copy ctor.
3 X copy ctor.
5 X copy ctor.
7 X copy ctor.
9
Vì nó có thể được đọc từ đầu ra, copy constructor cuộc gọi được thực hiện trong phạm vi có trụ sở lặp loop.
Điều này là do chúng ta đang nắm bắt các phần tử từ container theo giá trị
( auto x
phần trong for (auto x : v)
).
Đây là mã không hiệu quả , ví dụ, nếu các yếu tố này là trường hợp std::string
, phân bổ bộ nhớ heap có thể được thực hiện, với các chuyến đi đắt tiền đến trình quản lý bộ nhớ, v.v ... Điều này là vô ích nếu chúng ta chỉ muốn quan sát các yếu tố trong một container.
Vì vậy, một cú pháp tốt hơn có sẵn: chụp theo const
tham chiếu , nghĩa là const auto&
:
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (const auto& x : v)
{
cout << x << ' ';
}
Bây giờ đầu ra là:
[... copy constructor calls for vector<X> initialization ...]
Elements:
1 3 5 7 9
Không có bất kỳ cuộc gọi xây dựng sao chép giả (và có khả năng đắt tiền).
Vì vậy, khi quan sát các yếu tố trong một container (ví dụ, để truy cập read-only), cú pháp sau đây là tốt cho đơn giản giá rẻ-to-bản sao các loại, như int
, double
, v.v .:
for (auto elem : container)
Khác, chụp bằng const
tham chiếu là tốt hơn trong trường hợp chung , để tránh các cuộc gọi xây dựng sao chép vô dụng (và có khả năng tốn kém):
for (const auto& elem : container)
Sửa đổi các thành phần trong container
Nếu chúng ta muốn sửa đổi các thành phần trong một thùng chứa bằng cách sử dụng phạm vi for
, các
cú pháp for (auto elem : container)
và for (const auto& elem : container)
cú pháp ở trên là sai.
Trong thực tế, trong trường hợp trước, elem
lưu trữ một bản sao của phần tử gốc, vì vậy các sửa đổi được thực hiện đối với phần tử đó chỉ bị mất và không được lưu trữ liên tục trong vùng chứa, ví dụ:
vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v) // <-- capture by value (copy)
x *= 10; // <-- a local temporary copy ("x") is modified,
// *not* the original vector element.
for (auto x : v)
cout << x << ' ';
Đầu ra chỉ là chuỗi ban đầu:
1 3 5 7 9
Thay vào đó, một nỗ lực sử dụng for (const auto& x : v)
chỉ thất bại để biên dịch.
g ++ xuất ra một thông báo lỗi như thế này:
TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
x *= 10;
^
Cách tiếp cận đúng trong trường hợp này là bắt bằng cách không const
tham chiếu:
vector<int> v = {1, 3, 5, 7, 9};
for (auto& x : v)
x *= 10;
for (auto x : v)
cout << x << ' ';
Đầu ra là (như mong đợi):
10 30 50 70 90
for (auto& elem : container)
Cú pháp này cũng hoạt động đối với các loại phức tạp hơn, ví dụ: xem xét vector<string>
:
vector<string> v = {"Bob", "Jeff", "Connie"};
// Modify elements in place: use "auto &"
for (auto& x : v)
x = "Hi " + x + "!";
// Output elements (*observing* --> use "const auto&")
for (const auto& x : v)
cout << x << ' ';
đầu ra là:
Hi Bob! Hi Jeff! Hi Connie!
Trường hợp đặc biệt của trình vòng lặp proxy
Giả sử chúng ta có a vector<bool>
và chúng ta muốn đảo ngược trạng thái boolean logic của các phần tử của nó, sử dụng cú pháp trên:
vector<bool> v = {true, false, false, true};
for (auto& x : v)
x = !x;
Các mã trên không thể biên dịch.
g ++ xuất ra một thông báo lỗi tương tự như sau:
TestRangeFor.cpp:168:20: error: invalid initialization of non-const reference of
type 'std::_Bit_reference&' from an rvalue of type 'std::_Bit_iterator::referen
ce {aka std::_Bit_reference}'
for (auto& x : v)
^
Vấn đề là std::vector
mẫu được chuyên cho bool
, với một thực hiện mà gói các bool
s vào không gian tối ưu hóa (mỗi giá trị boolean được lưu trữ trong một chút, tám "boolean" bit trong một byte).
Do đó (vì không thể trả lại tham chiếu cho một bit),
vector<bool>
sử dụng mẫu được gọi là "trình lặp proxy" . "Trình lặp proxy" là một trình vòng lặp, khi được hủy đăng ký, không mang lại một thông thường bool &
, mà thay vào đó trả về (theo giá trị) một đối tượng tạm thời , là một lớp proxy có thể chuyển đổi thànhbool
. (Xem thêm câu hỏi này và các câu trả lời liên quan tại đây trên StackOverflow.)
Để sửa đổi các phần tử của vector<bool>
, auto&&
phải sử dụng một loại cú pháp mới (sử dụng ):
for (auto&& x : v)
x = !x;
Các mã sau hoạt động tốt:
vector<bool> v = {true, false, false, true};
// Invert boolean status
for (auto&& x : v) // <-- note use of "auto&&" for proxy iterators
x = !x;
// Print new element values
cout << boolalpha;
for (const auto& x : v)
cout << x << ' ';
và đầu ra:
false true true false
Lưu ý rằng for (auto&& elem : container)
cú pháp cũng hoạt động trong các trường hợp khác của các trình vòng lặp thông thường (không phải proxy) (ví dụ: a vector<int>
hoặc a vector<string>
).
(Như một lưu ý phụ, cú pháp "quan sát" đã nói ở trên for (const auto& elem : container)
cũng hoạt động tốt đối với trường hợp trình lặp proxy.)
Tóm lược
Các cuộc thảo luận ở trên có thể được tóm tắt trong các hướng dẫn sau:
Để quan sát các yếu tố, sử dụng cú pháp sau:
for (const auto& elem : container) // capture by const reference
Nếu các đối tượng rẻ để sao chép (như int
s, double
s, v.v.), có thể sử dụng một hình thức đơn giản hơn một chút:
for (auto elem : container) // capture by value
Để sửa đổi các yếu tố tại chỗ, sử dụng:
for (auto& elem : container) // capture by (non-const) reference
Nếu vùng chứa sử dụng "trình lặp proxy" (như std::vector<bool>
), hãy sử dụng:
for (auto&& elem : container) // capture by &&
Tất nhiên, nếu có nhu cầu tạo một bản sao cục bộ của phần tử bên trong thân vòng lặp, chụp theo giá trị ( for (auto elem : container)
) là một lựa chọn tốt.
Ghi chú bổ sung về mã chung
Trong mã chung , vì chúng ta không thể đưa ra các giả định về loại chung T
là rẻ để sao chép, trong chế độ quan sát, nó luôn an toàn để luôn sử dụng for (const auto& elem : container)
.
(Điều này sẽ không kích hoạt bản sao vô dụng khả năng tốn kém, sẽ chỉ làm việc tốt cũng với nhiều loại giá rẻ-to-bản sao như int
, và cũng cho container sử dụng proxy-lặp, giống như std::vector<bool>
.)
Hơn nữa, trong chế độ sửa đổi , nếu chúng ta muốn mã chung hoạt động trong trường hợp các trình lặp proxy, thì tùy chọn tốt nhất là for (auto&& elem : container)
.
(Điều này cũng sẽ hoạt động tốt đối với các container sử dụng các trình lặp không phải proxy thông thường, như std::vector<int>
hoặc std::vector<string>
.)
Vì vậy, trong mã chung , các hướng dẫn sau đây có thể được cung cấp:
Để quan sát các yếu tố, sử dụng:
for (const auto& elem : container)
Để sửa đổi các yếu tố tại chỗ, sử dụng:
for (auto&& elem : container)