วิธีที่ถูกต้องในการใช้ช่วงของ C ++ 11 คืออะไร?


211

วิธีที่ถูกต้องในการใช้ช่วงของ C ++ 11 คือforอะไร?

ควรใช้ไวยากรณ์ใด for (auto elem : container)หรือfor (auto& elem : container)หรือfor (const auto& elem : container)? หรืออื่น ๆ ?


6
ใช้การพิจารณาแบบเดียวกันกับอาร์กิวเมนต์ของฟังก์ชัน
Maxim Egorushkin

3
ที่จริงแล้วสิ่งนี้มีส่วนเกี่ยวข้องกับช่วงข้อมูลเพียงเล็กน้อย auto (const)(&) x = <expr>;เดียวกันสามารถกล่าวว่าจากการใด ๆ
Matthieu M.

2
@MatthieuM: นี้มีจำนวนมากจะทำอย่างไรกับช่วงที่ใช้สำหรับแน่นอน! พิจารณามือใหม่ที่เห็นไวยากรณ์หลาย ๆ แบบและไม่สามารถเลือกรูปแบบที่จะใช้ จุดของ "คำถาม & คำตอบ" คือพยายามที่จะทำให้กระจ่างบางและอธิบายความแตกต่างของบางกรณี (และหารือเกี่ยวกับคดีที่รวบรวมได้ดี แต่ไม่มีประสิทธิภาพเนื่องจากสำเนาลึก ๆ ที่ไร้ประโยชน์ ฯลฯ )
Mr.C64

2
@ Mr.C64: โดยทั่วไปแล้วฉันเกี่ยวข้องกับเรื่องนี้autoมากกว่าเกี่ยวกับช่วง คุณสามารถใช้ช่วงได้อย่างสมบูรณ์แบบโดยไม่ต้องมีauto! for (int i: v) {}ดีอย่างสมบูรณ์ แน่นอนว่าคะแนนส่วนใหญ่ที่คุณยกมาในคำตอบของคุณอาจเกี่ยวข้องกับประเภทมากกว่าคำว่าauto... แต่จากคำถามมันไม่ชัดเจนว่าจุดปวดอยู่ตรงไหน โดยส่วนตัวแล้วฉันจะแย่งเอาautoคำถามไป หรืออาจทำให้ชัดเจนว่าไม่ว่าคุณจะใช้autoหรือตั้งชื่อประเภทอย่างชัดเจนคำถามจะเน้นไปที่คุณค่า / การอ้างอิง
Matthieu M.

1
@ MatthieuM: ฉันเปิดเพื่อเปลี่ยนชื่อหรือแก้ไขคำถามในบางรูปแบบที่อาจทำให้ชัดเจนมากขึ้น ... อีกครั้งฉันมุ่งเน้นเพื่อหารือเกี่ยวกับตัวเลือกต่าง ๆ สำหรับช่วงตามไวยากรณ์ (แสดงรหัสที่รวบรวม แต่เป็น ไม่มีประสิทธิภาพรหัสที่ล้มเหลวในการรวบรวมและอื่น ๆ ) และพยายามที่จะให้คำแนะนำกับใครบางคน
Mr.C64

คำตอบ:


389

เรามาเริ่มแยกความแตกต่างระหว่างการสังเกตองค์ประกอบในภาชนะกับการดัดแปลงมันให้เข้าที่

การสังเกตองค์ประกอบ

ลองพิจารณาตัวอย่างง่ายๆ:

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)ทำงานที่ดียังสำหรับกรณีตัวทำซ้ำพร็อกซี.)

สรุป

การอภิปรายข้างต้นสามารถสรุปได้ในแนวทางต่อไปนี้:

  1. สำหรับการสังเกตองค์ประกอบให้ใช้ไวยากรณ์ต่อไปนี้:

    for (const auto& elem : container)    // capture by const reference
    • หากวัตถุมีราคาถูกในการคัดลอก (เช่นints, doubles, ฯลฯ ) เป็นไปได้ที่จะใช้รูปแบบที่เรียบง่ายเล็กน้อย:

      for (auto elem : container)    // capture by value
  2. สำหรับการปรับเปลี่ยนองค์ประกอบในสถานที่ให้ใช้:

    for (auto& elem : container)    // capture by (non-const) reference
    • หากคอนเทนเนอร์ใช้"proxy iterators" (เช่นstd::vector<bool>) ให้ใช้:

      for (auto&& elem : container)    // capture by &&

แน่นอนหากมีความจำเป็นต้องทำสำเนาองค์ประกอบภายในองค์ประกอบของลูปการจับภาพด้วยค่า ( 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>)

ดังนั้นในรหัสทั่วไปสามารถให้แนวทางต่อไปนี้:

  1. สำหรับการสังเกตองค์ประกอบให้ใช้:

    for (const auto& elem : container)
  2. สำหรับการปรับเปลี่ยนองค์ประกอบในสถานที่ให้ใช้:

    for (auto&& elem : container)

7
ไม่มีคำแนะนำสำหรับบริบททั่วไปใช่ไหม :(
R. Martinho Fernandes

11
ทำไมไม่เคยใช้auto&&? มีconst auto&&อะไรบ้าง
Martin Ba

1
ฉันคิดว่าคุณหายไปกรณีที่คุณต้องการสำเนาภายในวงจริงหรือไม่
juanchopanza

6
"ถ้าคอนเทนเนอร์ใช้" proxy iterators "" - และคุณรู้ว่ามันใช้ "proxy iterators" (ซึ่งอาจไม่ใช่กรณีในรหัสทั่วไป) ดังนั้นฉันคิดว่าสิ่งที่ดีที่สุดคือauto&&มันครอบคลุมได้auto&ดี
Christian Rau

5
ขอบคุณนั่นเป็น "การแนะนำหลักสูตรที่ผิดพลาด" ที่ยอดเยี่ยมสำหรับไวยากรณ์และเคล็ดลับบางอย่างสำหรับช่วงที่ใช้สำหรับโปรแกรมเมอร์ C # +1
AndrewJacksonZA

17

ไม่มีเป็นวิธีที่ถูกต้องในการใช้for (auto elem : container)หรือหรือfor (auto& elem : container) for (const auto& elem : container)คุณเพียงแค่แสดงสิ่งที่คุณต้องการ

ผมขออธิบายอย่างละเอียด เดินเล่นกัน

for (auto elem : container) ...

อันนี้คือน้ำตาลประโยคสำหรับ:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Observe that this is a copy by value.
    auto elem = *it;

}

คุณสามารถใช้อันนี้ถ้าภาชนะของคุณมีองค์ประกอบที่ถูกคัดลอก

for (auto& elem : container) ...

อันนี้คือน้ำตาลประโยคสำหรับ:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Now you're directly modifying the elements
    // because elem is an lvalue reference
    auto& elem = *it;

}

ใช้สิ่งนี้เมื่อคุณต้องการเขียนองค์ประกอบในคอนเทนเนอร์โดยตรงตัวอย่างเช่น

for (const auto& elem : container) ...

อันนี้คือน้ำตาลประโยคสำหรับ:

for(auto it = container.begin(); it != container.end(); ++it) {

    // You just want to read stuff, no modification
    const auto& elem = *it;

}

ในขณะที่ความคิดเห็นพูดเพียงเพื่ออ่าน และที่เกี่ยวกับมันทุกอย่าง "ถูกต้อง" เมื่อใช้อย่างถูกต้อง


2
ฉันตั้งใจจะให้คำแนะนำบางอย่างด้วยการรวบรวมรหัสตัวอย่าง (แต่ไม่มีประสิทธิภาพ) หรือไม่สามารถรวบรวมและอธิบายว่าทำไมและลองเสนอวิธีแก้ปัญหาบางอย่าง
Mr.C64

2
@ Mr.C64 โอ้ฉันขอโทษ - ฉันเพิ่งสังเกตเห็นว่านี่เป็นหนึ่งในคำถามที่พบบ่อยประเภท ฉันยังใหม่กับไซต์นี้ ขอโทษ! คำตอบของคุณเป็นสิ่งที่ดีที่ฉัน upvoted มัน - แต่ยังต้องการที่จะให้รุ่นรัดกุมมากขึ้นสำหรับผู้ที่ต้องการส่วนสำคัญของมัน หวังว่าฉันจะไม่บุกรุก

1
@ Mr.C64 มีปัญหาอะไรในการตอบคำถามด้วย OP? มันเป็นอีกคำตอบที่ถูกต้อง
mfontanini

1
@mfontanini: ไม่มีปัญหาแน่นอนถ้ามีคนโพสต์คำตอบดีกว่าของฉัน จุดประสงค์สุดท้ายคือให้การสนับสนุนคุณภาพแก่ชุมชน (โดยเฉพาะอย่างยิ่งสำหรับผู้เริ่มต้นที่อาจรู้สึกว่าหลงอยู่ตรงหน้าไวยากรณ์และตัวเลือกต่าง ๆ ที่ C ++ นำเสนอ)
Mr.C64

4

วิธีการที่ถูกต้องอยู่เสมอ

for(auto&& elem : container)

สิ่งนี้จะรับประกันการเก็บรักษาความหมายทั้งหมด


6
แต่ถ้าคอนเทนเนอร์ส่งคืนการอ้างอิงที่แก้ไขได้เท่านั้นและฉันต้องการทำให้ชัดเจนว่าฉันไม่ต้องการแก้ไขในลูป ฉันไม่ควรใช้auto const &เพื่อทำให้ความตั้งใจของฉันชัดเจนหรือไม่?
RedX

@RedX: "การอ้างอิงที่แก้ไขได้" คืออะไร?
Lightness Races ในวงโคจร

2
@RedX: การอ้างอิงจะไม่มีวันconstและจะไม่มีการเปลี่ยนแปลง อย่างไรก็ตามคำตอบของคุณคือใช่ฉัน
Lightness Races ในวงโคจร

4
ในขณะที่สิ่งนี้อาจใช้งานได้ฉันรู้สึกว่านี่เป็นคำแนะนำที่ไม่ดีเมื่อเทียบกับวิธีการที่เหมาะสมยิ่งขึ้นและได้รับการพิจารณาโดยคำตอบที่ยอดเยี่ยมและครอบคลุมของ Mr.C64 ที่ระบุข้างต้น การลดตัวหารสามัญน้อยที่สุดไม่ใช่สิ่งที่ C ++ มีไว้สำหรับ
Jack Aidley

6
ข้อเสนอวิวัฒนาการภาษานี้เห็นด้วยกับคำตอบ "ไม่ดี" นี้: open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm
Luc Hermitte

1

ในขณะที่แรงจูงใจเริ่มต้นของการวนรอบสำหรับการวนซ้ำนั้นอาจง่ายกว่าการวนซ้ำองค์ประกอบของคอนเทนเนอร์ไวยากรณ์จะเป็นแบบทั่วไปมากพอที่จะเป็นประโยชน์แม้กับวัตถุที่ไม่ใช่คอนเทนเนอร์ล้วนๆ

ความต้องการทางวากยสัมพันธ์สำหรับ for-loop นั้นrange_expressionสนับสนุนbegin()และend()เป็นทั้งสองฟังก์ชัน - ไม่ว่าจะเป็นฟังก์ชันสมาชิกประเภทที่ประเมินหรือเป็นฟังก์ชันที่ไม่ใช่สมาชิกสิ่งที่ใช้อินสแตนซ์ของประเภท

เป็นตัวอย่างที่วางแผนไว้เราสามารถสร้างช่วงของตัวเลขและวนซ้ำในช่วงโดยใช้คลาสต่อไปนี้

struct Range
{
   struct Iterator
   {
      Iterator(int v, int s) : val(v), step(s) {}

      int operator*() const
      {
         return val;
      }

      Iterator& operator++()
      {
         val += step;
         return *this;
      }

      bool operator!=(Iterator const& rhs) const
      {
         return (this->val < rhs.val);
      }

      int val;
      int step;
   };

   Range(int l, int h, int s=1) : low(l), high(h), step(s) {}

   Iterator begin() const
   {
      return Iterator(low, step);
   }

   Iterator end() const
   {
      return Iterator(high, 1);
   }

   int low, high, step;
}; 

ด้วยmainฟังก์ชั่นต่อไปนี้

#include <iostream>

int main()
{
   Range r1(1, 10);
   for ( auto item : r1 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r2(1, 20, 2);
   for ( auto item : r2 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r3(1, 20, 3);
   for ( auto item : r3 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;
}

จะได้รับผลลัพธ์ต่อไปนี้

1 2 3 4 5 6 7 8 9 
1 3 5 7 9 11 13 15 17 19 
1 4 7 10 13 16 19 
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.