การทำความเข้าใจความหมายของคำศัพท์และแนวคิด - RAII (Resource Acquisition is Initialization)


110

คุณช่วยนักพัฒนา C ++ ช่วยให้คำอธิบายที่ดีแก่เราได้ไหมว่า RAII คืออะไรทำไมจึงสำคัญและอาจมีความเกี่ยวข้องกับภาษาอื่นหรือไม่

ผมทำรู้นิด ๆ หน่อย ๆ ฉันเชื่อว่าย่อมาจาก "Resource Acquisition is Initialization" อย่างไรก็ตามชื่อนั้นไม่ได้ทำให้สับสนกับความเข้าใจ (อาจไม่ถูกต้อง) ของฉันเกี่ยวกับสิ่งที่ RAII คือ: ฉันรู้สึกว่า RAII เป็นวิธีเริ่มต้นวัตถุบนสแต็กดังนั้นเมื่อตัวแปรเหล่านั้นออกไปนอกขอบเขตผู้ทำลายจะโดยอัตโนมัติ เรียกว่าทำให้ทรัพยากรถูกล้าง

เหตุใดจึงไม่เรียกว่า "ใช้สแต็กเพื่อทริกเกอร์การล้างข้อมูล" (UTSTTC :)? คุณจะเดินทางจากที่นั่นไปยัง "RAII" ได้อย่างไร?

และคุณจะสร้างบางสิ่งบนสแต็กที่จะทำให้เกิดการล้างบางสิ่งที่อาศัยอยู่บนฮีปได้อย่างไร? นอกจากนี้ยังมีกรณีที่คุณไม่สามารถใช้ RAII ได้หรือไม่? คุณเคยคิดว่าตัวเองอยากเก็บขยะไหม? อย่างน้อยคนเก็บขยะที่คุณสามารถใช้สำหรับวัตถุบางอย่างในขณะที่ปล่อยให้คนอื่นจัดการ?

ขอบคุณ.


27
UTSTTC? ฉันชอบมัน! มันใช้งานง่ายกว่า RAII มาก RAII มีชื่อไม่ดีฉันสงสัยว่าโปรแกรมเมอร์ C ++ จะโต้แย้งเรื่องนั้น แต่มันไม่ง่ายที่จะเปลี่ยน ;)
jalf

10
นี่คือมุมมองของ Stroustrup เกี่ยวกับเรื่องนี้groups.google.com/group/comp.lang.c++.moderated/msg/…
SBI

3
@sbi: อย่างไรก็ตาม +1 ในความคิดเห็นของคุณเพียงเพื่อการวิจัยทางประวัติศาสตร์ ฉันเชื่อว่าการมีมุมมองของผู้แต่ง (B. Stroustrup) เกี่ยวกับชื่อแนวคิด (RAII) นั้นน่าสนใจพอที่จะมีคำตอบ
paercebal

1
@paercebal: การวิจัยทางประวัติศาสตร์? ตอนนี้คุณทำให้ฉันรู้สึกแก่มากแล้ว :(ฉันอ่านกระทู้ทั้งหมดในตอนนั้นและไม่ได้คิดว่าตัวเองเป็นมือใหม่ C ++!
sbi

3
+1 ฉันกำลังจะถามคำถามเดียวกันดีใจที่ฉันไม่ใช่คนเดียวที่เข้าใจแนวคิดนี้ แต่ไม่เข้าใจชื่อ ดูเหมือนว่าควรเรียกว่า RAOI - Resource Acquisition On Initialization
laurent

คำตอบ:


132

เหตุใดจึงไม่เรียกว่า "ใช้สแต็กเพื่อทริกเกอร์การล้างข้อมูล" (UTSTTC :)?

RAII กำลังบอกคุณว่าต้องทำอะไร: รับทรัพยากรของคุณในตัวสร้าง! ฉันจะเพิ่ม: หนึ่งทรัพยากรหนึ่งตัวสร้าง UTSTTC เป็นเพียงแอปพลิเคชั่นเดียว RAII ยังมีอีกมากมาย

การจัดการทรัพยากรห่วย ที่นี่ทรัพยากรคือสิ่งที่ต้องล้างข้อมูลหลังการใช้งาน การศึกษาโครงการในหลายแพลตฟอร์มแสดงให้เห็นว่าจุดบกพร่องส่วนใหญ่เกี่ยวข้องกับการจัดการทรัพยากร - และเป็นเรื่องที่ไม่ดีอย่างยิ่งใน Windows (เนื่องจากวัตถุและตัวจัดสรรหลายประเภท)

ใน C ++ การจัดการทรัพยากรมีความซับซ้อนเป็นพิเศษเนื่องจากการรวมกันของข้อยกเว้นและเทมเพลต (สไตล์ C ++) หากต้องการดูใต้ฝากระโปรงโปรดดูที่GOTW8 )


C ++ รับประกันว่า destructor ถูกเรียกใช้ในกรณีที่ตัวสร้างประสบความสำเร็จเท่านั้น ด้วยเหตุนี้ RAII จึงสามารถแก้ปัญหาที่น่ารังเกียจหลายอย่างที่โปรแกรมเมอร์ทั่วไปอาจไม่รู้ด้วยซ้ำ นี่คือตัวอย่างบางส่วนนอกเหนือจาก "ตัวแปรในพื้นที่ของฉันจะถูกทำลายเมื่อใดก็ตามที่ฉันกลับมา"

เริ่มต้นด้วยFileHandleคลาสที่เรียบง่ายเกินไปที่ใช้ RAII:

class FileHandle
{
    FILE* file;

public:

    explicit FileHandle(const char* name)
    {
        file = fopen(name);
        if (!file)
        {
            throw "MAYDAY! MAYDAY";
        }
    }

    ~FileHandle()
    {
        // The only reason we are checking the file pointer for validity
        // is because it might have been moved (see below).
        // It is NOT needed to check against a failed constructor,
        // because the destructor is NEVER executed when the constructor fails!
        if (file)
        {
            fclose(file);
        }
    }

    // The following technicalities can be skipped on the first read.
    // They are not crucial to understanding the basic idea of RAII.
    // However, if you plan to implement your own RAII classes,
    // it is absolutely essential that you read on :)



    // It does not make sense to copy a file handle,
    // hence we disallow the otherwise implicitly generated copy operations.

    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;



    // The following operations enable transfer of ownership
    // and require compiler support for rvalue references, a C++0x feature.
    // Essentially, a resource is "moved" from one object to another.

    FileHandle(FileHandle&& that)
    {
        file = that.file;
        that.file = 0;
    }

    FileHandle& operator=(FileHandle&& that)
    {
        file = that.file;
        that.file = 0;
        return *this;
    }
}

หากการก่อสร้างล้มเหลว (มีข้อยกเว้น) จะไม่มีการเรียกใช้ฟังก์ชันสมาชิกอื่นแม้แต่ตัวทำลาย

RAII หลีกเลี่ยงการใช้วัตถุในสถานะที่ไม่ถูกต้อง มันทำให้ชีวิตง่ายขึ้นก่อนที่เราจะใช้วัตถุด้วยซ้ำ

ตอนนี้ให้เราดูวัตถุชั่วคราว:

void CopyFileData(FileHandle source, FileHandle dest);

void Foo()
{
    CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest"));
}

มีข้อผิดพลาดสามกรณีที่ต้องจัดการ: ไม่สามารถเปิดไฟล์ได้สามารถเปิดได้เพียงไฟล์เดียวสามารถเปิดทั้งสองไฟล์ได้ แต่การคัดลอกไฟล์ล้มเหลว ในการใช้งานที่ไม่ใช่ RAII Fooจะต้องจัดการทั้งสามกรณีอย่างชัดเจน

RAII เผยแพร่ทรัพยากรที่ได้มาแม้ว่าจะได้รับทรัพยากรหลายรายการภายในคำสั่งเดียวก็ตาม

ตอนนี้ให้เรารวมวัตถุบางอย่าง:

class Logger
{
    FileHandle original, duplex;   // this logger can write to two files at once!

public:

    Logger(const char* filename1, const char* filename2)
    : original(filename1), duplex(filename2)
    {
        if (!filewrite_duplex(original, duplex, "New Session"))
            throw "Ugh damn!";
    }
}

ตัวสร้างของLoggerจะล้มเหลวหากตัวoriginalสร้างล้มเหลว (เนื่องจากfilename1ไม่สามารถเปิดได้) ตัวduplexสร้างของล้มเหลว (เนื่องจากfilename2ไม่สามารถเปิดได้) หรือการเขียนไปยังไฟล์ภายในLoggerตัวสร้างของตัวสร้างล้มเหลว ไม่ว่าในกรณีใด ๆ เหล่านี้Loggerจะไม่มีการเรียกตัวทำลายล้าง- ดังนั้นเราจึงไม่สามารถพึ่งพาตัวLoggerทำลายของเพื่อปล่อยไฟล์ได้ แต่ถ้าoriginalถูกสร้างขึ้นตัวทำลายของมันจะถูกเรียกในระหว่างการล้างตัวLoggerสร้าง

RAII ทำให้การล้างข้อมูลง่ายขึ้นหลังจากการสร้างบางส่วน


จุดลบ:

จุดลบ? ปัญหาทั้งหมดสามารถแก้ไขได้ด้วย RAII และตัวชี้สมาร์ท ;-)

บางครั้ง RAII ก็ไม่สะดวกเมื่อคุณต้องการการได้มาซึ่งล่าช้าโดยการผลักวัตถุที่รวมเข้าไว้ในฮีป
ลองนึกภาพว่า Logger ต้องการไฟล์SetTargetFile(const char* target). ในกรณีนั้นแฮนเดิลที่ยังคงต้องเป็นสมาชิกLoggerจำเป็นต้องอยู่บนฮีป (เช่นในตัวชี้อัจฉริยะเพื่อเรียกการทำลายแฮนเดิลอย่างเหมาะสม)

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

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


หมายเหตุเกี่ยวกับตัวอย่าง FileHandle: มันไม่ได้ตั้งใจให้สมบูรณ์เป็นเพียงแค่ตัวอย่าง - แต่กลับกลายเป็นว่าไม่ถูกต้อง ขอบคุณ Johannes Schaub ที่ชี้ให้เห็นและ FredOverflow สำหรับการเปลี่ยนเป็นโซลูชัน C ++ 0x ที่ถูกต้อง เมื่อเวลาผ่านไปฉันได้ตกลงกับวิธีการที่เอกสารที่นี่


1
+1 สำหรับชี้ว่า GC และ ASAP ไม่เชื่อมโยงกัน ไม่เจ็บบ่อย แต่เมื่อวินิจฉัยไม่ง่าย: /
Matthieu M.

10
ประโยคหนึ่งโดยเฉพาะที่ฉันมองข้ามไปในการอ่านก่อนหน้านี้ คุณบอกว่า "RAII" กำลังบอกคุณว่า "ได้รับทรัพยากรของคุณจากผู้สร้าง" มันสมเหตุสมผลและเกือบจะเป็นการถอดความแบบคำต่อคำของ "RAII" ตอนนี้ฉันทำได้ดียิ่งขึ้น (ฉันจะโหวตให้คุณอีกครั้งถ้าฉันทำได้ :)
Charlie Flowers

2
ข้อได้เปรียบที่สำคัญอย่างหนึ่งของ GC คือเฟรมเวิร์กการจัดสรรหน่วยความจำสามารถป้องกันการสร้างการอ้างอิงแบบห้อยในกรณีที่ไม่มีโค้ด "ไม่ปลอดภัย" (หากอนุญาตให้ใช้โค้ด "ไม่ปลอดภัย" แน่นอนว่าเฟรมเวิร์กไม่สามารถป้องกันอะไรได้เลย) GC มักจะเหนือกว่า RAII เมื่อจัดการกับวัตถุที่ไม่เปลี่ยนรูปแบบที่ใช้ร่วมกันเช่นสตริงซึ่งมักไม่มีเจ้าของที่ชัดเจนและไม่จำเป็นต้องล้างข้อมูล โชคไม่ดีที่เฟรมเวิร์กอื่น ๆ ไม่พยายามรวม GC และ RAII เนื่องจากแอปพลิเคชันส่วนใหญ่จะมีการผสมผสานระหว่างวัตถุที่ไม่เปลี่ยนรูป (ซึ่ง GC จะดีที่สุด) และวัตถุที่ต้องการการล้างข้อมูล (โดยที่ RAII ดีที่สุด)
supercat

@supercat: โดยทั่วไปฉันชอบ GC - แต่ใช้ได้กับแหล่งข้อมูลที่ GC "เข้าใจ" เท่านั้น เช่น. NET GC ไม่ทราบต้นทุนของวัตถุ COM เมื่อเพียงแค่สร้างและทำลายพวกมันแบบวนซ้ำมันจะทำให้แอปพลิเคชั่นทำงานได้อย่างมีความสุขเกี่ยวกับพื้นที่แอดเดรสหรือหน่วยความจำเสมือน - ไม่ว่าอะไรจะมาก่อน - โดยไม่คิดที่จะทำ GC --- ยิ่งไปกว่านั้นแม้จะอยู่ในสภาพแวดล้อม GC ที่สมบูรณ์แบบ แต่ฉันก็ยังพลาดพลังแห่งการทำลายล้างที่กำหนดไว้: คุณสามารถใช้รูปแบบเดียวกันกับงานศิลปะอื่น ๆ เช่นการแสดงองค์ประกอบ UI ภายใต้เงื่อนไขของผู้รับรอง
peterchen

@peterchen: สิ่งหนึ่งที่ฉันคิดว่าไม่มีอยู่ในความคิดที่เกี่ยวข้องกับ OOP จำนวนมากคือแนวคิดเรื่องการเป็นเจ้าของวัตถุ การติดตามความเป็นเจ้าของมักมีความจำเป็นอย่างชัดเจนสำหรับวัตถุที่มีทรัพยากร แต่ก็มักจำเป็นสำหรับวัตถุที่เปลี่ยนแปลงไม่ได้ที่ไม่มีทรัพยากร โดยทั่วไปออบเจ็กต์ควรห่อหุ้มสถานะที่เปลี่ยนแปลงไม่ได้ทั้งในการอ้างอิงถึงอ็อบเจ็กต์ที่ไม่เปลี่ยนรูปที่อาจแบ่งใช้หรือในอ็อบเจ็กต์ที่เปลี่ยนแปลงไม่ได้ซึ่งเป็นเจ้าของ แต่เพียงผู้เดียว ความเป็นเจ้าของ แต่เพียงผู้เดียวดังกล่าวไม่จำเป็นต้องหมายความถึงการเข้าถึงการเขียน แต่เพียงผู้เดียว แต่ถ้าFooเป็นเจ้าของBarและBozกลายพันธุ์ ...
supercat

42

มีคำตอบที่ยอดเยี่ยมอยู่แล้วดังนั้นฉันจึงเพิ่มบางสิ่งที่ลืมไป

0. RAII เป็นเรื่องเกี่ยวกับขอบเขต

RAII เกี่ยวกับทั้งสองอย่าง:

  1. การรับทรัพยากร (ไม่ว่าทรัพยากรใดก็ตาม) ในตัวสร้างและยกเลิกการรับทรัพยากรในตัวทำลาย
  2. มีตัวสร้างดำเนินการเมื่อมีการประกาศตัวแปรและตัวทำลายจะดำเนินการโดยอัตโนมัติเมื่อตัวแปรอยู่นอกขอบเขต

คนอื่นตอบไปแล้วดังนั้นฉันจะไม่อธิบายอย่างละเอียด

1. เมื่อเขียนโค้ดใน Java หรือ C # แสดงว่าคุณใช้ RAII อยู่แล้ว ...

MONSIEUR JOURDAIN: อะไรนะ! เมื่อฉันพูดว่า "นิโคลนำรองเท้าแตะมาให้ฉันและให้หมวกคลุมผมด้วย" นั่นเป็นร้อยแก้วใช่ไหม

ปรมาจารย์ปรัชญา: ครับท่าน

MONSIEUR JOURDAIN: เป็นเวลากว่าสี่สิบปีแล้วที่ฉันพูดเป็นร้อยแก้วโดยไม่รู้อะไรเกี่ยวกับเรื่องนี้และฉันมีหน้าที่ต้องสอนคุณอย่างมาก

- โมเลียร์: สุภาพบุรุษชนชั้นกลาง, องก์ที่ 2, ฉากที่ 4

อย่างที่ Monsieur Jourdain ทำกับร้อยแก้ว C # และแม้แต่คน Java ก็ใช้ RAII อยู่แล้ว แต่ในทางที่ซ่อนเร้น ตัวอย่างเช่นโค้ด Java ต่อไปนี้ (ซึ่งเขียนในลักษณะเดียวกันใน C # โดยแทนที่synchronizedด้วยlock):

void foo()
{
   // etc.

   synchronized(someObject)
   {
      // if something throws here, the lock on someObject will
      // be unlocked
   }

   // etc.
}

... กำลังใช้ RAII อยู่แล้ว: การได้มาของ mutex เสร็จสิ้นในคีย์เวิร์ด ( synchronizedหรือlock) และการยกเลิกการได้มาจะเสร็จสิ้นเมื่อออกจากขอบเขต

มันเป็นธรรมชาติมากในสัญกรณ์มันแทบจะไม่ต้องมีคำอธิบายแม้แต่คนที่ไม่เคยได้ยินเกี่ยวกับ RAII

ข้อได้เปรียบที่ C ++ มีเหนือ Java และ C # ที่นี่คือทุกอย่างสามารถทำได้โดยใช้ RAII ตัวอย่างเช่นไม่มี build-in ที่เทียบเท่าsynchronizedหรือlockใน C ++ โดยตรง แต่เรายังสามารถมีได้

ใน C ++ จะเขียนว่า:

void foo()
{
   // etc.

   {
      Lock lock(someObject) ; // lock is an object of type Lock whose
                              // constructor acquires a mutex on
                              // someObject and whose destructor will
                              // un-acquire it 

      // if something throws here, the lock on someObject will
      // be unlocked
   }

   // etc.
}

ซึ่งสามารถเขียนได้อย่างง่ายดาย Java / C # way (โดยใช้มาโคร C ++):

void foo()
{
   // etc.

   LOCK(someObject)
   {
      // if something throws here, the lock on someObject will
      // be unlocked
   }

   // etc.
}

2. RAII มีการใช้งานอื่น

WHITE RABBIT: [ร้องเพลง] ฉันมาสาย / ฉันมาสาย / สำหรับเดทที่สำคัญมาก / ไม่มีเวลาพูด "สวัสดี" / ลาก่อน. / ฉันมาสายฉันมาสายฉันมาสาย

- Alice in Wonderland (เวอร์ชั่นดิสนีย์ปี 2494)

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

ตัวอย่างเช่นคุณสามารถเขียนวัตถุตอบโต้ (ฉันปล่อยให้เป็นแบบฝึกหัด) และใช้เพียงแค่ประกาศตัวแปรเช่นเดียวกับที่ใช้วัตถุล็อคด้านบน:

void foo()
{
   double timeElapsed = 0 ;

   {
      Counter counter(timeElapsed) ;
      // do something lengthy
   }
   // now, the timeElapsed variable contain the time elapsed
   // from the Counter's declaration till the scope exit
}

ซึ่งแน่นอนว่าสามารถเขียนอีกครั้งวิธี Java / C # โดยใช้มาโคร:

void foo()
{
   double timeElapsed = 0 ;

   COUNTER(timeElapsed)
   {
      // do something lengthy
   }
   // now, the timeElapsed variable contain the time elapsed
   // from the Counter's declaration till the scope exit
}

3. ทำไม C ++ จึงขาดfinally?

[ตะโกน] นับถอยหลังครั้งสุดท้าย !

- ยุโรป: การนับถอยหลังครั้งสุดท้าย (ขออภัยฉันไม่มีคำพูดที่นี่ ... :-)

finallyข้อถูกนำมาใช้ใน C # / Java เพื่อกำจัดทรัพยากรที่จับในกรณีของขอบเขตออก (ทั้งผ่านreturnหรือยกเว้นโยน)

ผู้อ่านข้อกำหนด Astute จะสังเกตเห็นว่า C ++ ไม่มีประโยคในที่สุด และนี่ไม่ใช่ข้อผิดพลาดเนื่องจากไม่จำเป็นต้องใช้ C ++ เนื่องจาก RAII จัดการการกำจัดทรัพยากรแล้ว (และเชื่อฉันเถอะว่าการเขียนตัวทำลาย C ++ นั้นง่ายกว่าการเขียน Java ที่ถูกต้องในที่สุดหรือแม้แต่วิธี Dispose ที่ถูกต้องของ C #)

ถึงกระนั้นบางครั้งfinallyประโยคก็ดูดี เราสามารถทำได้ใน C ++ หรือไม่? ใช่เราทำได้! และอีกครั้งด้วยการใช้ RAII แบบอื่น

สรุป: RAII เป็นมากกว่าปรัชญาใน C ++ นั่นคือ C ++

ไรงี้? นี่คือ C ++ !!!

- ความคิดเห็นที่ขุ่นเคืองของนักพัฒนา C ++ คัดลอกโดยกษัตริย์ Sparta ผู้ปิดบังและเพื่อน 300 คนของเขาอย่างไร้ยางอาย

เมื่อคุณมาถึงระดับของประสบการณ์ใน C ++ บางคุณเริ่มคิดในแง่ของRAIIในแง่ของการดำเนินการ construtors และ destructors อัตโนมัติ

คุณเริ่มคิดในแง่ของขอบเขตและ{และ}ตัวอักษรกลายเป็นคนที่สำคัญที่สุดในรหัสของคุณ

และเกือบทุกอย่างเหมาะสมในแง่ของ RAII: ยกเว้นความปลอดภัย, mutexes, การเชื่อมต่อฐานข้อมูล, การร้องขอฐานข้อมูล, การเชื่อมต่อเซิร์ฟเวอร์, นาฬิกา, ที่จับระบบปฏิบัติการ ฯลฯ และสุดท้าย แต่ไม่ท้ายสุดคือหน่วยความจำ

ส่วนฐานข้อมูลนั้นไม่สำคัญเลยเช่นถ้าคุณยอมจ่ายราคาคุณสามารถเขียนในรูปแบบ " การเขียนโปรแกรมธุรกรรม " เรียกใช้บรรทัดและบรรทัดของโค้ดจนกว่าจะตัดสินใจในท้ายที่สุดหากคุณต้องการที่จะทำการเปลี่ยนแปลงทั้งหมด หรือถ้าเป็นไปไม่ได้ให้เปลี่ยนกลับการเปลี่ยนแปลงทั้งหมดกลับไป (ตราบเท่าที่แต่ละบรรทัดเป็นไปตามการรับประกันข้อยกเว้นที่เข้มงวดเป็นอย่างน้อย) (ดูส่วนที่สองของบทความHerb's Sutterสำหรับการเขียนโปรแกรมธุรกรรม)

และเหมือนปริศนาทุกอย่างลงตัว

RAII เป็นส่วนหนึ่งของ C ++ มาก C ++ ไม่สามารถเป็น C ++ ได้หากไม่มีมัน

สิ่งนี้อธิบายว่าเหตุใดนักพัฒนา C ++ ที่มีประสบการณ์จึงหลงใหล RAII และเหตุใด RAII จึงเป็นสิ่งแรกที่พวกเขาค้นหาเมื่อลองใช้ภาษาอื่น

และอธิบายได้ว่าทำไม Garbage Collector ในขณะที่เป็นเทคโนโลยีที่ยอดเยี่ยมในตัวมันจึงไม่น่าประทับใจนักจากมุมมองของนักพัฒนา C ++:

  • RAII จัดการกรณีส่วนใหญ่ที่จัดการโดย GC อยู่แล้ว
  • GC จัดการได้ดีกว่า RAII ด้วยการอ้างอิงแบบวงกลมบนอ็อบเจ็กต์ที่มีการจัดการที่บริสุทธิ์ (ลดลงโดยการใช้จุดอ่อนอย่างชาญฉลาด)
  • GC ยังคงถูก จำกัด ไว้ที่หน่วยความจำในขณะที่ RAII สามารถจัดการทรัพยากรประเภทใดก็ได้
  • ตามที่อธิบายไว้ข้างต้น RAII สามารถทำอะไรได้อีกมากมาย ...

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

16

1
บางส่วนก็ตรงกับคำถามของฉัน แต่การค้นหาไม่ตอบสนองหรือรายการ "คำถามที่เกี่ยวข้อง" ซึ่งปรากฏขึ้นหลังจากที่คุณป้อนคำถามใหม่ ขอบคุณสำหรับลิงค์
Charlie Flowers

1
@ ชาร์ลี: การสร้างในการค้นหาอ่อนแอมากในบางวิธี การใช้ไวยากรณ์แท็ก ("[หัวข้อ]") มีประโยชน์มากและหลาย ๆ คนก็ใช้ google ...
dmckee --- ex-moderator kitten

10

RAII ใช้ความหมายของ C ++ destructors เพื่อจัดการทรัพยากร ตัวอย่างเช่นพิจารณาตัวชี้อัจฉริยะ คุณมีตัวสร้างพารามิเตอร์ของตัวชี้ที่เริ่มต้นตัวชี้นี้ด้วยที่อยู่ของวัตถุ คุณจัดสรรตัวชี้บนสแต็ก:

SmartPointer pointer( new ObjectClass() );

เมื่อตัวชี้อัจฉริยะอยู่นอกขอบเขตตัวทำลายของคลาสตัวชี้จะลบวัตถุที่เชื่อมต่อ ตัวชี้มีการจัดสรรกองซ้อนและวัตถุ - จัดสรรกอง

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


2
ดังนั้นจึงควรเรียกว่า UCDSTMR :)
Daniel Daranas

ในความคิดที่สองฉันคิดว่า UDSTMR เหมาะสมกว่า มีการกำหนดภาษา (C ++) ดังนั้นจึงไม่จำเป็นต้องใช้ตัวอักษร "C" ในตัวย่อ UDSTMR ย่อมาจาก Using Destructor Semantics To Manage Resources
Daniel Daranas

9

ฉันต้องการให้คำตอบก่อนหน้านี้ชัดเจนกว่านี้

RAII, Resource Acquisition Is Initializationหมายถึงทรัพยากรที่ได้มาทั้งหมดควรได้มาในบริบทของการเริ่มต้นของวัตถุ สิ่งนี้ห้ามการได้มาซึ่งทรัพยากรแบบ "เปล่า" เหตุผลคือการล้างข้อมูลใน C ++ ทำงานบนพื้นฐานของวัตถุไม่ใช่พื้นฐานการเรียกใช้ฟังก์ชัน ดังนั้นการล้างข้อมูลทั้งหมดควรทำโดยวัตถุไม่ใช่การเรียกใช้ฟังก์ชัน ในแง่นี้ C ++ จึงมุ่งเน้นไปที่วัตถุมากกว่าเช่น Java การล้างข้อมูลบน Java ขึ้นอยู่กับการเรียกใช้ฟังก์ชันในส่วนfinallyคำสั่ง


คำตอบที่ดี และ "initialization of an object" หมายถึง "constrctors" ใช่หรือไม่?
Charlie Flowers

@ ชาร์ลี: ใช่โดยเฉพาะอย่างยิ่งในกรณีนี้
MSalters

8

ฉันเห็นด้วยกับ cpitis แต่อยากจะเพิ่มว่าทรัพยากรสามารถเป็นอะไรก็ได้ไม่ใช่แค่หน่วยความจำ ทรัพยากรอาจเป็นไฟล์ส่วนสำคัญเธรดหรือการเชื่อมต่อฐานข้อมูล

เรียกว่า Resource Acquisition Is Initialization เนื่องจากทรัพยากรจะได้มาเมื่อออบเจ็กต์ที่ควบคุมทรัพยากรถูกสร้างขึ้นถ้าตัวสร้างล้มเหลว (เช่นเนื่องจากข้อยกเว้น) ทรัพยากรจะไม่ได้มา จากนั้นเมื่อวัตถุอยู่นอกขอบเขตทรัพยากรจะถูกปล่อย c ++ รับประกันว่าอ็อบเจ็กต์ทั้งหมดบนสแต็กที่สร้างสำเร็จจะถูกทำลาย (ซึ่งรวมถึงคอนสตรัคเตอร์ของคลาสพื้นฐานและสมาชิกแม้ว่าคอนสตรัคเตอร์ระดับซุปเปอร์จะล้มเหลวก็ตาม)

เหตุผลเบื้องหลัง RAII คือการทำให้ข้อยกเว้นการได้มาซึ่งทรัพยากรปลอดภัย ทรัพยากรทั้งหมดที่ได้มาจะถูกปล่อยออกมาอย่างเหมาะสมไม่ว่าจะมีข้อยกเว้นเกิดขึ้น อย่างไรก็ตามสิ่งนี้ขึ้นอยู่กับคุณภาพของคลาสที่ได้รับทรัพยากร (สิ่งนี้ต้องเป็นข้อยกเว้นที่ปลอดภัยและเป็นเรื่องยาก)


เยี่ยมมากขอขอบคุณที่อธิบายเหตุผลเบื้องหลังชื่อ ตามที่ฉันเข้าใจคุณอาจถอดความ RAII เป็น "อย่าได้รับทรัพยากรใด ๆ ผ่านกลไกอื่นใดนอกจากการเริ่มต้น (ตามตัวสร้าง)" ใช่?
Charlie Flowers

ใช่นี่เป็นนโยบายของฉันอย่างไรก็ตามฉันระมัดระวังในการเขียนคลาส RAII ของตัวเองเนื่องจากต้องปลอดภัยเป็นพิเศษ เมื่อฉันเขียนพวกเขาฉันพยายามที่จะมั่นใจในความปลอดภัยโดยการใช้คลาส RAII อื่น ๆ ที่เขียนโดยผู้เชี่ยวชาญ
iain

ฉันไม่พบว่าพวกเขาเขียนยาก หากชั้นเรียนของคุณมีขนาดเล็กเพียงพอก็ไม่ยากเลย
Rob K

7

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


4
ปัญหาไม่ได้อยู่ที่ตัวกำหนดเท่านั้น ปัญหาที่แท้จริงคือ Finalizers (การตั้งชื่อ java) เข้ามาขัดขวาง GC GC มีประสิทธิภาพเนื่องจากไม่เรียกคืนวัตถุที่ตายแล้ว แต่จะเพิกเฉยต่อการลืมเลือน GC ต้องติดตามออบเจ็กต์ด้วย
Finalizers

1
ยกเว้นใน java / c # คุณอาจจะล้างข้อมูลในบล็อกสุดท้ายแทนที่จะเป็นใน Finalizer
jk.

4

RAII มาจาก Resource Allocation Is Initialization โดยทั่วไปหมายความว่าเมื่อคอนสตรัคเตอร์เสร็จสิ้นการดำเนินการอ็อบเจ็กต์ที่สร้างขึ้นจะถูกเตรียมใช้งานโดยสมบูรณ์และพร้อมใช้งาน นอกจากนี้ยังบอกเป็นนัยว่าผู้ทำลายจะปล่อยทรัพยากรใด ๆ (เช่นหน่วยความจำทรัพยากรระบบปฏิบัติการ) ที่เป็นของวัตถุ

เมื่อเทียบกับภาษา / เทคโนโลยีที่เก็บขยะ (เช่น Java, .NET) C ++ ช่วยให้สามารถควบคุมอายุการใช้งานของวัตถุได้อย่างสมบูรณ์ สำหรับสแต็กที่จัดสรรอ็อบเจ็กต์คุณจะทราบว่าเมื่อใดที่ตัวทำลายของอ็อบเจ็กต์จะถูกเรียก (เมื่อการดำเนินการออกไปนอกขอบเขต) สิ่งที่ไม่ได้รับการควบคุมในกรณีของการรวบรวมขยะ แม้แต่การใช้ตัวชี้อัจฉริยะใน C ++ (เช่น boost :: shared_ptr) คุณจะรู้ว่าเมื่อไม่มีการอ้างอิงถึงวัตถุปลายแหลมตัวทำลายของวัตถุนั้นจะถูกเรียก


3

และคุณจะสร้างบางสิ่งบนสแต็กที่จะทำให้เกิดการล้างบางสิ่งที่อาศัยอยู่บนฮีปได้อย่างไร?

class int_buffer
{
   size_t m_size;
   int *  m_buf;

   public:
   int_buffer( size_t size )
     : m_size( size ), m_buf( 0 )
   {
       if( m_size > 0 )
           m_buf = new int[m_size]; // will throw on failure by default
   }
   ~int_buffer()
   {
       delete[] m_buf;
   }
   /* ...rest of class implementation...*/

};


void foo() 
{
    int_buffer ib(20); // creates a buffer of 20 bytes
    std::cout << ib.size() << std::endl;
} // here the destructor is called automatically even if an exception is thrown and the memory ib held is freed.

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

class mutex
{
   // ...
   take();
   release();

   class mutex::sentry
   {
      mutex & mm;
      public:
      sentry( mutex & m ) : mm(m) 
      {
          mm.take();
      }
      ~sentry()
      {
          mm.release();
      }
   }; // mutex::sentry;
};
mutex m;

int getSomeValue()
{
    mutex::sentry ms( m ); // blocks here until the mutex is taken
    return 0;  
} // the mutex is released in the destructor call here.

นอกจากนี้ยังมีกรณีที่คุณไม่สามารถใช้ RAII ได้หรือไม่?

ไม่ไม่จริงๆ

คุณเคยคิดว่าตัวเองอยากเก็บขยะไหม? อย่างน้อยคนเก็บขยะที่คุณสามารถใช้สำหรับวัตถุบางอย่างในขณะที่ปล่อยให้คนอื่นจัดการ?

ไม่เลย การรวบรวมขยะจะแก้ปัญหาการจัดการทรัพยากรแบบไดนามิกเพียงเล็กน้อยเท่านั้น


ฉันใช้ Java และ C # น้อยมากดังนั้นฉันจึงไม่เคยพลาด แต่ GC ทำให้สไตล์ของฉันแคบลงเมื่อพูดถึงการจัดการทรัพยากรเมื่อฉันต้องใช้เพราะฉันไม่สามารถใช้ RAII ได้
Rob K

1
ฉันใช้ C # มากและเห็นด้วยกับคุณ 100% ในความเป็นจริงฉันถือว่า GC ที่ไม่ใช่ปัจจัยกำหนดเป็นความรับผิดในภาษาหนึ่ง ๆ
Nemanja Trifunovic

2

มีคำตอบที่ดีมากมายที่นี่ แต่ฉันอยากจะเพิ่ม:
คำอธิบายง่ายๆของ RAII คือใน C ++ วัตถุที่จัดสรรบนสแต็กจะถูกทำลายเมื่อใดก็ตามที่มันอยู่นอกขอบเขต นั่นหมายความว่าจะมีการเรียกตัวทำลายวัตถุและสามารถทำการล้างข้อมูลที่จำเป็นทั้งหมดได้
ซึ่งหมายความว่าหากวัตถุถูกสร้างขึ้นโดยไม่มี "ใหม่" ไม่จำเป็นต้องมี "ลบ" และนี่คือแนวคิดเบื้องหลัง "ตัวชี้อัจฉริยะ" - พวกมันอาศัยอยู่บนสแต็กและโดยพื้นฐานแล้วจะห่อหุ้มวัตถุที่เป็นฮีป


1
ไม่พวกเขาไม่ทำ แต่คุณมีเหตุผลที่ดีในการสร้างตัวชี้อัจฉริยะบนฮีปหรือไม่? อย่างไรก็ตามตัวชี้อัจฉริยะเป็นเพียงตัวอย่างของจุดที่ RAII มีประโยชน์
E Dominique

1
บางทีการใช้ "stack" กับ "heap" ของฉันอาจจะดูเลอะเทอะไปหน่อย - โดยวัตถุบน "stack" ฉันหมายถึงวัตถุในพื้นที่ มันสามารถเป็นส่วนหนึ่งของวัตถุได้ตามธรรมชาติเช่นบนกอง ด้วยการ "สร้างตัวชี้อัจฉริยะบนฮีป" ฉันหมายถึงการใช้ใหม่ / ลบบนตัวชี้อัจฉริยะเอง
E Dominique

1

RAII เป็นคำย่อของ Resource Acquisition Is Initialization

เทคนิคนี้มีลักษณะเฉพาะอย่างมากสำหรับ C ++ เนื่องจากการสนับสนุนทั้ง Constructors & Destructors และเกือบจะโดยอัตโนมัติตัวสร้างที่จับคู่อาร์กิวเมนต์ที่ถูกส่งผ่านหรือในกรณีที่เลวร้ายที่สุดตัวสร้างเริ่มต้นจะเรียกว่า & destructors หากความชัดเจนที่ระบุจะถูกเรียกเป็นอย่างอื่นค่าเริ่มต้น ที่เพิ่มโดยคอมไพเลอร์ C ++ เรียกว่าหากคุณไม่ได้เขียนตัวทำลายอย่างชัดเจนสำหรับคลาส C ++ สิ่งนี้เกิดขึ้นเฉพาะกับอ็อบเจ็กต์ C ++ ที่มีการจัดการอัตโนมัติซึ่งหมายความว่าไม่ได้ใช้ที่เก็บฟรี (หน่วยความจำที่จัดสรร / ยกเลิกการจัดสรรโดยใช้ตัวดำเนินการใหม่ [] ใหม่ / ลบลบ [] ตัวดำเนินการ C ++)

เทคนิค RAII ใช้คุณลักษณะอ็อบเจ็กต์ที่จัดการอัตโนมัตินี้เพื่อจัดการอ็อบเจ็กต์ที่สร้างบนฮีป / ฟรีสโตร์โดยขอหน่วยความจำเพิ่มเติมโดยใช้ new / new [] อย่างชัดเจนซึ่งควรถูกทำลายอย่างชัดเจนโดยการเรียกลบ / ลบ [] . คลาสของอ็อบเจ็กต์ที่จัดการโดยอัตโนมัติจะรวมอ็อบเจ็กต์อื่นที่สร้างขึ้นบนหน่วยความจำฮีป / ที่เก็บฟรี ดังนั้นเมื่อเรียกใช้คอนสตรัคเตอร์ของอ็อบเจ็กต์ที่มีการจัดการอัตโนมัติอ็อบเจ็กต์ที่ถูกห่อจะถูกสร้างขึ้นบนหน่วยความจำฮีป / ที่จัดเก็บฟรีและเมื่อแฮนเดิลของอ็อบเจ็กต์ที่จัดการโดยอัตโนมัติอยู่นอกขอบเขตตัวทำลายของอ็อบเจ็กต์ที่มีการจัดการอัตโนมัตินั้นจะถูกเรียกโดยอัตโนมัติในที่ที่ห่อ วัตถุถูกทำลายโดยใช้การลบ ด้วยแนวคิด OOP หากคุณรวมวัตถุดังกล่าวไว้ในคลาสอื่นในขอบเขตส่วนตัวคุณจะไม่สามารถเข้าถึงสมาชิกและวิธีการและ นี่คือเหตุผลว่าทำไมพอยน์เตอร์อัจฉริยะ (หรือที่เรียกว่าคลาสจัดการ) จึงได้รับการออกแบบมาสำหรับ ตัวชี้อัจฉริยะเหล่านี้แสดงวัตถุที่ห่อไว้เป็นวัตถุที่พิมพ์ไปยังโลกภายนอกและที่นั่นโดยอนุญาตให้เรียกใช้สมาชิก / วิธีการใด ๆ ที่วัตถุหน่วยความจำที่เปิดเผยถูกสร้างขึ้น โปรดทราบว่าคำแนะนำที่ชาญฉลาดมีหลากหลายรสชาติตามความต้องการที่แตกต่างกัน คุณควรอ้างถึงการเขียนโปรแกรม Modern C ++ โดย Andrei Alexandrescu หรือเพิ่มประสิทธิภาพของไลบรารี (www.boostorg) shared_ptr.hpp การใช้งาน / เอกสารประกอบเพื่อเรียนรู้เพิ่มเติมเกี่ยวกับเรื่องนี้ หวังว่านี่จะช่วยให้คุณเข้าใจ RAII คุณควรอ้างถึงการเขียนโปรแกรม Modern C ++ โดย Andrei Alexandrescu หรือเพิ่มประสิทธิภาพของไลบรารี (www.boostorg) shared_ptr.hpp การใช้งาน / เอกสารประกอบเพื่อเรียนรู้เพิ่มเติมเกี่ยวกับเรื่องนี้ หวังว่านี่จะช่วยให้คุณเข้าใจ RAII คุณควรอ้างถึงการเขียนโปรแกรม Modern C ++ โดย Andrei Alexandrescu หรือเพิ่มประสิทธิภาพของไลบรารี (www.boostorg) shared_ptr.hpp การใช้งาน / เอกสารประกอบเพื่อเรียนรู้เพิ่มเติมเกี่ยวกับเรื่องนี้ หวังว่านี่จะช่วยให้คุณเข้าใจ RAII

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.