ใครคือผู้ถูกตำหนิสำหรับช่วงนี้ซึ่งอ้างอิงมาจากการอ้างอิงชั่วคราว


15

รหัสต่อไปนี้ดูไม่เป็นอันตรายเมื่อพบเห็นครั้งแรก ผู้ใช้ใช้ฟังก์ชันbar()เพื่อโต้ตอบกับฟังก์ชันไลบรารีบางอย่าง (ซึ่งอาจจะมีการทำงานถึงแม้จะเป็นเวลานานตั้งแต่bar()กลับอ้างอิงถึงค่าที่ไม่ใช่ชั่วคราวหรือคล้ายกัน.) Bแต่ตอนนี้มันเป็นเพียงการกลับตัวอย่างใหม่ของ Bอีกครั้งมีฟังก์ชั่นที่ผลตอบแทนอ้างอิงกับวัตถุชนิดa() iterateable Aผู้ใช้ต้องการสอบถามวัตถุนี้ซึ่งนำไปสู่ ​​segfault เนื่องจากBวัตถุชั่วคราวที่ส่งคืนโดยbar()ถูกทำลายก่อนที่จะเริ่มซ้ำ

ฉันไม่แน่ใจว่าใคร (ห้องสมุดหรือผู้ใช้) ที่จะตำหนิสำหรับสิ่งนี้ ห้องสมุดทั้งหมดที่มีให้ชั้นเรียนดูสะอาดสำหรับฉันและแน่นอนว่าไม่ได้ทำอะไรที่แตกต่างกัน (การส่งคืนการอ้างอิงถึงสมาชิก, การคืนอินสแตนซ์สแต็ก, ... ) กว่าโค้ดอื่น ๆ ผู้ใช้ดูเหมือนจะไม่ทำอะไรผิดปกติเช่นกันเขาแค่ทำซ้ำวัตถุบางอย่างโดยไม่ทำอะไรเกี่ยวกับวัตถุนั้นตลอดชีวิต

(คำถามที่เกี่ยวข้องอาจเป็น: หนึ่งควรสร้างกฎทั่วไปว่ารหัสไม่ควร "ช่วงตามสำหรับ - ซ้ำ" เหนือสิ่งที่ถูกดึงโดยมากกว่าหนึ่งสายที่ถูกล่ามโซ่ในส่วนหัววนเนื่องจากการโทรใด ๆ เหล่านี้อาจกลับ rvalue?)

#include <algorithm>
#include <iostream>

// "Library code"
struct A
{
    A():
        v{0,1,2}
    {
        std::cout << "A()" << std::endl;
    }

    ~A()
    {
        std::cout << "~A()" << std::endl;
    }

    int * begin()
    {
        return &v[0];
    }

    int * end()
    {
        return &v[3];
    }

    int v[3];
};

struct B
{
    A m_a;

    A & a()
    {
        return m_a;
    }
};

B bar()
{
    return B();
}

// User code
int main()
{
    for( auto i : bar().a() )
    {
        std::cout << i << std::endl;
    }
}

6
เมื่อคุณคิดได้ว่าใครจะโทษใครจะเป็นขั้นตอนต่อไป ตะโกนใส่เขา / เธอ?
JensG

7
ไม่ฉันจะทำไม จริง ๆ แล้วฉันสนใจที่จะทราบว่ากระบวนการคิดของการพัฒนา "โปรแกรม" นี้ไม่สามารถหลีกเลี่ยงปัญหานี้ได้ในอนาคต
hllnll

สิ่งนี้ไม่เกี่ยวกับค่า rvalues ​​หรือ range-based สำหรับลูป แต่ผู้ใช้ไม่เข้าใจอายุการใช้งานของออบเจ็กต์อย่างถูกต้อง
James

หมายเหตุไซต์: นี่คือCWG 900ซึ่งถูกปิดเป็น Not A Defect บางทีนาทีอาจมีการสนทนาบ้าง
dyp 9'14 น

8
ใครจะถูกตำหนิในเรื่องนี้? Bjarne Stroustrup และ Dennis Ritchie เป็นคนแรกและสำคัญที่สุด
Mason Wheeler

คำตอบ:


14

ฉันคิดว่าปัญหาพื้นฐานคือการรวมกันของคุณสมบัติภาษา (หรือขาดมัน) ของ C ++ ทั้งรหัสห้องสมุดและรหัสลูกค้ามีความสมเหตุสมผล (ตามหลักฐานที่แสดงว่าปัญหาอยู่ไกลจากความชัดเจน) หากอายุการใช้งานของชั่วคราวBมีความเหมาะสมขยาย (ไปยังจุดสิ้นสุดของลูป) ก็จะไม่มีปัญหา

การทำให้ชีวิตขมับเพียงพอและไม่นานเป็นเรื่องยากมาก ไม่ได้เป็นโฆษณาเพียงอย่างเดียว“ ทุกขมับที่เกี่ยวข้องในการสร้างช่วงสำหรับช่วงชีวิตสำหรับการใช้งานจนถึงจุดสิ้นสุดของลูป” จะไม่มีผลข้างเคียง พิจารณากรณีของการB::a()ส่งคืนช่วงที่เป็นอิสระจากBวัตถุตามค่า จากนั้นชั่วคราวBสามารถยกเลิกได้ทันที แม้ว่าจะสามารถระบุกรณีที่จำเป็นต้องมีการขยายอายุการใช้งานได้อย่างแม่นยำเนื่องจากกรณีเหล่านี้ไม่ชัดเจนสำหรับโปรแกรมเมอร์ผลกระทบ (ตัวทำลายที่เรียกว่ามากในภายหลัง) จะน่าแปลกใจและอาจเป็นแหล่งที่มาของข้อบกพร่องที่บอบบาง

มันจะเป็นที่พึงปรารถนามากกว่าที่จะตรวจจับและห้ามเรื่องไร้สาระเช่นนั้นบังคับให้โปรแกรมเมอร์เพื่อยกระดับbar()ตัวแปรท้องถิ่นอย่างชัดเจน สิ่งนี้เป็นไปไม่ได้ใน C ++ 11 และอาจเป็นไปไม่ได้เพราะต้องมีคำอธิบายประกอบ สนิมทำสิ่งนี้ซึ่งลายเซ็นของ.a()จะเป็น:

fn a<'x>(bar: &'x B) -> &'x A { bar.a }
// If we make it as explicit as possible, or
fn a(&self) -> &A { self.a }
// if we make it a method and rely on lifetime elision.

นี่'xคือตัวแปรอายุการใช้งานหรือภูมิภาคซึ่งเป็นชื่อเชิงสัญลักษณ์สำหรับรอบระยะเวลาที่ทรัพยากรพร้อมใช้งาน ตรงไปตรงมามีชีวิตยากที่จะอธิบาย - หรือเรายังไม่ได้คิดออกคำอธิบายที่ดีที่สุด - ดังนั้นฉันจะ จำกัด ตัวเองให้ต่ำสุดที่จำเป็นเช่นนี้และดูผู้อ่านโน้มเอียงที่จะเอกสารอย่างเป็นทางการ

ผู้ตรวจสอบการยืมจะสังเกตเห็นว่าผลลัพธ์ของbar().a()ความต้องการอยู่ตราบใดที่ลูปทำงาน เรียบเรียงเป็นข้อ จำกัด เกี่ยวกับอายุการใช้งานที่เราเขียน:'x 'loop <= 'xนอกจากนี้ยังจะสังเกตเห็นว่าผู้รับของการเรียกวิธีการbar(), เป็นชั่วคราว พอยน์เตอร์สองตัวนั้นสัมพันธ์กับอายุการใช้งานที่เท่ากันดังนั้นจึง'x <= 'tempเป็นข้อ จำกัด อื่น

ข้อ จำกัด สองข้อนี้ขัดแย้งกัน! เราต้องการ'loop <= 'x <= 'tempแต่'temp <= 'loopสิ่งที่จับปัญหาได้ค่อนข้างแม่นยำ เนื่องจากข้อกำหนดที่ขัดแย้งกันรหัสรถที่ถูกปฏิเสธ โปรดทราบว่านี่เป็นการตรวจสอบเวลาคอมไพล์และรหัส Rust มักส่งผลให้รหัสเครื่องเดียวกันกับรหัส C ++ เทียบเท่าดังนั้นคุณไม่จำเป็นต้องเสียค่าใช้จ่ายในการดำเนินการ

อย่างไรก็ตามนี่เป็นคุณสมบัติที่ยิ่งใหญ่ในการเพิ่มภาษาและใช้ได้เฉพาะกับรหัสทั้งหมดที่ใช้ การออกแบบของ APIs ก็มีผลเช่นกัน (การออกแบบบางอย่างที่อันตรายเกินไปใน C ++ สามารถนำไปใช้งานได้ อนิจจานั่นหมายความว่ามันไม่มีประโยชน์ที่จะเพิ่มลงใน C ++ (หรือภาษาใด ๆ ) ย้อนหลัง โดยสรุปแล้วความผิดพลาดเกิดขึ้นกับภาษาที่ประสบความสำเร็จและความจริงที่ว่า Bjarne ในปี 1983 ไม่มีลูกแก้วและการมองการณ์ไกลที่จะรวมบทเรียนของการวิจัย 30 ปีที่ผ่านมาและประสบการณ์ C ++ ;-)

แน่นอนว่ามันไม่มีประโยชน์อะไรเลยในการหลีกเลี่ยงปัญหาในอนาคต (เว้นแต่คุณจะเปลี่ยนเป็นสนิมและไม่ต้องใช้ C ++ อีกต่อไป) หนึ่งสามารถหลีกเลี่ยงการแสดงออกอีกต่อไปด้วยวิธีการที่ถูกผูกมัดหลายสาย (ซึ่งค่อนข้าง จำกัด และไม่ได้แก้ไขปัญหาตลอดชีวิตจากระยะไกล) หรืออาจจะลองการนำนโยบายการเป็นเจ้าของมีระเบียบวินัยมากขึ้นโดยความช่วยเหลือของคอมไพเลอร์: เอกสารอย่างชัดเจนว่าbarผลตอบแทนตามมูลค่าและผลจากการB::a()ต้องไม่ได้อายุยืนBที่a()ถูกเรียก เมื่อเปลี่ยนฟังก์ชั่นการกลับมาด้วยค่าแทนการอ้างอิงอีกต่อไปชีวิตมีสติว่านี่คือการเปลี่ยนแปลงของสัญญา ยังคงเกิดข้อผิดพลาดได้ง่าย แต่อาจช่วยให้กระบวนการระบุสาเหตุได้เร็วขึ้น


14

เราสามารถแก้ปัญหานี้โดยใช้คุณสมบัติ C ++ ได้หรือไม่?

C ++ 11 ได้เพิ่มฟังก์ชันสมาชิก ref-qualifiers ซึ่งอนุญาตให้ จำกัด ประเภทค่าของคลาสอินสแตนซ์ (นิพจน์) ที่ฟังก์ชันสมาชิกสามารถเรียกใช้ได้ ตัวอย่างเช่น:

struct foo {
    void bar() & {} // lvalue-ref-qualified
};

foo& lvalue ();
foo  prvalue();

lvalue ().bar(); // OK
prvalue().bar(); // error

เมื่อเรียกใช้beginฟังก์ชันสมาชิกเรารู้ว่าส่วนใหญ่เราจะต้องเรียกendฟังก์ชันสมาชิก (หรือบางอย่างเช่นsizeเพื่อให้ได้ขนาดของช่วง) สิ่งนี้ต้องการให้เราดำเนินการกับค่า lvalue เนื่องจากเราจำเป็นต้องจัดการสองครั้ง คุณสามารถยืนยันได้ว่าฟังก์ชั่นสมาชิกเหล่านี้ควรมีคุณสมบัติการรับรอง lvalue

อย่างไรก็ตามสิ่งนี้อาจไม่สามารถแก้ปัญหาพื้นฐานได้: aliasing beginและendฟังก์ชั่นสมาชิกนามแฝงวัตถุหรือทรัพยากรในการจัดการโดยวัตถุ หากเราแทนที่beginและendด้วยฟังก์ชั่นเดียวrangeเราควรจัดเตรียมหนึ่งฟังก์ชันที่สามารถเรียกใช้บนค่า rvalues:

struct foo {
    vector<int> arr;

    auto range() & // C++14 return type deduction for brevity
    { return std::make_pair(arr.begin(), arr.end()); }
};

for(auto const& e : foo().range()) // error

นี่อาจเป็นกรณีการใช้งานที่ถูกต้อง แต่คำนิยามข้างต้นrangeไม่อนุญาต เนื่องจากเราไม่สามารถระบุที่อยู่ชั่วคราวหลังจากการเรียกฟังก์ชันสมาชิกอาจมีเหตุผลมากกว่าที่จะส่งคืนคอนเทนเนอร์เช่นช่วงการเป็นเจ้าของ:

struct foo {
    vector<int> arr;

    auto range() &
    { return std::make_pair(arr.begin(), arr.end()); }

    auto range() &&
    { return std::move(arr); }
};

for(auto const& e : foo().range()) // OK

ใช้สิ่งนี้กับเคสของ OP และตรวจสอบโค้ดเล็กน้อย

struct B {
    A m_a;
    A & a() { return m_a; }
};

ฟังก์ชันนี้สมาชิกเปลี่ยนประเภทค่าของนิพจน์: B()เป็น prvalue แต่B().a()เป็น lvalue ในทางกลับกันB().m_aค่า rvalue ดังนั้นเริ่มต้นด้วยการทำให้สิ่งนี้สอดคล้องกัน มีสองวิธีในการทำสิ่งนี้:

struct B {
    A m_a;
    A &  a() &  { return m_a; }

    A && a() && { return std::move(m_a); }
    // or
    A    a() && { return std::move(m_a); }
};

รุ่นที่สองตามที่กล่าวข้างต้นจะแก้ไขปัญหาใน OP

นอกจากนี้เราสามารถ จำกัดBฟังก์ชั่นของสมาชิก:

struct A {
    // [...]

    int * begin() & { return &v[0]; }
    int * end  () & { return &v[3]; }

    int v[3];
};

สิ่งนี้จะไม่มีผลกระทบใด ๆ กับรหัสของ OP เนื่องจากผลลัพธ์ของนิพจน์หลังจาก:อยู่ในช่วงสำหรับลูปถูกผูกไว้กับตัวแปรอ้างอิง และตัวแปรนี้ (เป็นนิพจน์ที่ใช้ในการเข้าถึงbeginและendฟังก์ชั่นสมาชิก) เป็นค่า lvalue

แน่นอนคำถามคือว่าหรือไม่กฎเริ่มต้นที่ควรจะเป็น"aliasing ฟังก์ชั่นสมาชิกใน rvalues ควรกลับวัตถุซึ่งเป็นเจ้าของทรัพยากรทั้งหมดของมันเว้นแต่มีเหตุผลที่ดีที่จะไม่" นามแฝงที่ส่งคืนสามารถใช้อย่างถูกต้องตามกฎหมาย แต่เป็นอันตรายในวิธีที่คุณประสบ: ไม่สามารถใช้เพื่อยืดอายุการใช้งานของ "ผู้ปกครอง" ชั่วคราว:

// using the OP's definition of `struct B`,
// or version 1, `A && a() &&;`

A&&      a = B().a(); // bug: binds directly, dangling reference
A const& a = B().a(); // bug: same as above
A        a = B().a(); // OK

A&&      a = B().m_a; // OK: extends the lifetime of the temporary

ใน C ++ 2a ฉันคิดว่าคุณควรจะแก้ไขปัญหานี้ (หรือคล้ายกัน) ดังนี้:

for( B b = bar(); auto i : b.a() )

แทนของ OP

for( auto i : bar().a() )

วิธีแก้ปัญหาด้วยตนเองระบุว่าอายุการใช้งานของbคือบล็อกทั้งหมดของ for-loop

ข้อเสนอที่แนะนำให้รู้จักกับแถลงการณ์นี้

การสาธิตสด


ดูเพิ่มเติมที่: open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4101.html#120
dyp
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.