เรามาเริ่มแยกความแตกต่างระหว่างการสังเกตองค์ประกอบในภาชนะกับการดัดแปลงมันให้เข้าที่
การสังเกตองค์ประกอบ
ลองพิจารณาตัวอย่างง่ายๆ:
vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)
cout << x << ' ';
โค้ดด้านบนจะพิมพ์องค์ประกอบต่างint
ๆ ในvector
:
1 3 5 7 9
ตอนนี้ให้พิจารณาอีกกรณีหนึ่งซึ่งองค์ประกอบเวกเตอร์ไม่ใช่แค่จำนวนเต็มอย่างง่าย แต่อินสแตนซ์ของคลาสที่ซับซ้อนยิ่งขึ้นพร้อมตัวสร้างสำเนาที่กำหนดเอง ฯลฯ
// 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;
}
ถ้าเราใช้for (auto x : v) {...}
ไวยากรณ์ข้างต้นกับคลาสใหม่นี้:
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (auto x : v)
{
cout << x << ' ';
}
ผลลัพธ์เป็นดังนี้:
[... 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
เนื่องจากสามารถอ่านได้จากเอาต์พุตการเรียกใช้ตัวสร้างการคัดลอกจะทำระหว่างช่วงตามการวนซ้ำ
นี่เป็นเพราะเรากำลังจับองค์ประกอบจากภาชนะตามค่า
( auto x
ส่วนหนึ่งในfor (auto x : v)
)
สิ่งนี้ไม่มีประสิทธิภาพรหัสที่เช่นถ้าองค์ประกอบเหล่านี้เป็นอินสแตนซ์ของการstd::string
จัดสรรหน่วยความจำฮีปสามารถทำได้ด้วยการเดินทางไปยังตัวจัดการหน่วยความจำที่มีราคาแพง ฯลฯ สิ่งนี้ไม่มีประโยชน์หากเราแค่ต้องการสังเกตองค์ประกอบในคอนเทนเนอร์
ดังนั้นมีไวยากรณ์ที่ดีกว่า: จับภาพโดยconst
อ้างอิงคือconst auto&
:
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (const auto& x : v)
{
cout << x << ' ';
}
ตอนนี้ผลลัพธ์คือ:
[... copy constructor calls for vector<X> initialization ...]
Elements:
1 3 5 7 9
ไม่มีการเรียกตัวสร้างการคัดลอก (และอาจมีราคาแพง) ปลอม
ดังนั้นเมื่อสังเกตองค์ประกอบในคอนเทนเนอร์ (เช่นสำหรับการเข้าถึงแบบอ่านอย่างเดียว) ไวยากรณ์ต่อไปนี้เป็นสิ่งที่ดีสำหรับประเภทcheap-to-copyอย่างเช่นint
, เช่นdouble
, ฯลฯ :
for (auto elem : container)
มิฉะนั้นการจับภาพโดยconst
การอ้างอิงจะดีกว่าในกรณีทั่วไปเพื่อหลีกเลี่ยงการเรียกตัวสร้างการคัดลอกที่ไม่มีประโยชน์ (และอาจมีราคาแพง):
for (const auto& elem : container)
การปรับเปลี่ยนองค์ประกอบในภาชนะ
ถ้าเราต้องการแก้ไของค์ประกอบในคอนเทนเนอร์โดยใช้ช่วงตามfor
ข้างบนfor (auto elem : container)
และfor (const auto& elem : container)
ไวยากรณ์ผิด
ในความเป็นจริงในกรณีก่อนหน้านี้elem
เก็บสำเนาขององค์ประกอบดั้งเดิมดังนั้นการแก้ไขที่ทำกับมันจะหายไปและไม่ได้เก็บไว้ในภาชนะเช่น:
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 << ' ';
เอาต์พุตเป็นเพียงลำดับเริ่มต้น:
1 3 5 7 9
แต่ความพยายามในการใช้for (const auto& x : v)
ก็ไม่สามารถรวบรวมได้
g ++ แสดงข้อความแสดงข้อผิดพลาดดังนี้:
TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
x *= 10;
^
วิธีการที่ถูกต้องในกรณีนี้คือการจับโดยไม่const
อ้างอิง:
vector<int> v = {1, 3, 5, 7, 9};
for (auto& x : v)
x *= 10;
for (auto x : v)
cout << x << ' ';
ผลลัพธ์คือ (ตามที่คาดไว้):
10 30 50 70 90
for (auto& elem : container)
ไวยากรณ์นี้ใช้งานได้กับประเภทที่ซับซ้อนมากขึ้นเช่นพิจารณา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 << ' ';
ผลลัพธ์คือ:
Hi Bob! Hi Jeff! Hi Connie!
กรณีพิเศษของตัววนซ้ำพร็อกซี
สมมติว่าเรามีvector<bool>
และเราต้องการที่จะกลับสถานะตรรกะบูลีนขององค์ประกอบโดยใช้ไวยากรณ์ข้างต้น:
vector<bool> v = {true, false, false, true};
for (auto& x : v)
x = !x;
รหัสข้างต้นล้มเหลวในการรวบรวม
g ++ เอาต์พุตข้อความแสดงข้อผิดพลาดคล้ายกับสิ่งนี้:
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)
^
ปัญหาคือว่าstd::vector
แม่แบบเฉพาะสำหรับbool
ที่มีการดำเนินงานที่แพ็คbool
เพื่อเพิ่มประสิทธิภาพของพื้นที่ (แต่ละค่าบูลีนถูกเก็บไว้ในหนึ่งบิตแปด "บูล" บิตในไบต์)
เพราะการที่ (เพราะมันเป็นไปไม่ได้ที่จะกลับมามีการอ้างอิงถึงบิตเดียว)
vector<bool>
ใช้สิ่งที่เรียกว่า"iterator พร็อกซี่"รูปแบบ A "iterator พร็อกซี่" เป็น iterator ว่าเมื่อ dereferenced ไม่ได้ผลผลิตสามัญbool &
แต่ผลตอบแทน (โดยค่าบริการ) วัตถุชั่วคราวซึ่งเป็นระดับพร็อกซี่bool
ที่จะแปลงสภาพ (โปรดดูคำถามนี้และคำตอบที่เกี่ยวข้องที่นี่ใน StackOverflow)
ในการแก้ไของค์ประกอบของจะต้องใช้vector<bool>
ไวยากรณ์รูปแบบใหม่ (โดยใช้auto&&
):
for (auto&& x : v)
x = !x;
รหัสต่อไปนี้ทำงานได้ดี:
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 << ' ';
และเอาท์พุท:
false true true false
โปรดทราบว่าfor (auto&& elem : container)
ไวยากรณ์ยังทำงานในกรณีอื่น ๆ ของตัววนซ้ำธรรมดา (ที่ไม่ใช่พร็อกซี) (เช่นสำหรับ a vector<int>
หรือ avector<string>
)
(ในฐานะที่เป็นบันทึกด้านข้าง, ไวยากรณ์ "สังเกต" ดังกล่าวข้างต้นของการfor (const auto& elem : container)
ทำงานที่ดียังสำหรับกรณีตัวทำซ้ำพร็อกซี.)
สรุป
การอภิปรายข้างต้นสามารถสรุปได้ในแนวทางต่อไปนี้:
สำหรับการสังเกตองค์ประกอบให้ใช้ไวยากรณ์ต่อไปนี้:
for (const auto& elem : container) // capture by const reference
หากวัตถุมีราคาถูกในการคัดลอก (เช่นint
s, double
s, ฯลฯ ) เป็นไปได้ที่จะใช้รูปแบบที่เรียบง่ายเล็กน้อย:
for (auto elem : container) // capture by value
สำหรับการปรับเปลี่ยนองค์ประกอบในสถานที่ให้ใช้:
for (auto& elem : container) // capture by (non-const) reference
แน่นอนหากมีความจำเป็นต้องทำสำเนาองค์ประกอบภายในองค์ประกอบของลูปการจับภาพด้วยค่า ( for (auto elem : container)
) เป็นตัวเลือกที่ดี
หมายเหตุเพิ่มเติมเกี่ยวกับรหัสทั่วไป
ในรหัสทั่วไปเนื่องจากเราไม่สามารถทำให้สมมติฐานเกี่ยวกับประเภททั่วไปT
เป็นราคาถูกคัดลอกในการสังเกตfor (const auto& elem : container)
โหมดมันปลอดภัยที่จะใช้งานเสมอ
(สิ่งนี้จะไม่เรียกสำเนาที่ไม่มีประโยชน์ราคาแพงอาจใช้ได้ดีสำหรับประเภทราคาถูกเพื่อคัดลอกเช่นint
และสำหรับตู้คอนเทนเนอร์ที่ใช้ proxy-iterator เช่นstd::vector<bool>
)
นอกจากนี้ในการปรับเปลี่ยนโหมดถ้าเราต้องการรหัสทั่วไปในการทำงานนอกจากนี้ในกรณีของร็อกซี่-iterators for (auto&& elem : container)
ตัวเลือกที่ดีที่สุดคือ
(สิ่งนี้จะทำงานได้ดีสำหรับคอนเทนเนอร์ที่ใช้ธรรมดาที่ไม่ใช่ proxy-iterators เช่นstd::vector<int>
หรือstd::vector<string>
)
ดังนั้นในรหัสทั่วไปสามารถให้แนวทางต่อไปนี้:
สำหรับการสังเกตองค์ประกอบให้ใช้:
for (const auto& elem : container)
สำหรับการปรับเปลี่ยนองค์ประกอบในสถานที่ให้ใช้:
for (auto&& elem : container)