Hiểu ý nghĩa của thuật ngữ và khái niệm - RAII (Chuyển đổi tài nguyên là Khởi tạo)


110

Các nhà phát triển C ++ có thể vui lòng cho chúng tôi một mô tả tốt về RAII là gì, tại sao nó lại quan trọng và liệu nó có thể có liên quan đến các ngôn ngữ khác hay không?

Tôi làm biết một chút. Tôi tin rằng nó là viết tắt của "Resource Acquisition is Initialization". Tuy nhiên, cái tên đó không phù hợp với hiểu biết (có thể không chính xác) của tôi về RAII là gì: Tôi có ấn tượng rằng RAII là một cách khởi tạo các đối tượng trên ngăn xếp sao cho khi các biến đó vượt ra ngoài phạm vi, các hàm hủy sẽ tự động được gọi là nguyên nhân làm sạch tài nguyên.

Vậy tại sao điều đó không được gọi là "sử dụng ngăn xếp để kích hoạt dọn dẹp" (UTSTTC :)? Làm thế nào để bạn đi từ đó đến "RAII"?

Và làm thế nào bạn có thể tạo ra một thứ gì đó trên ngăn xếp để dọn dẹp thứ gì đó sống trên đống? Ngoài ra, có trường hợp nào bạn không thể sử dụng RAII không? Bạn có bao giờ thấy mình ước ao được thu gom rác không? Ít nhất bạn có thể sử dụng một bộ thu gom rác cho một số đối tượng trong khi để những người khác được quản lý?

Cảm ơn.


27
UTSTTC? Tôi thích nó! Nó trực quan hơn RAII rất nhiều. RAII xấu được đặt tên, tôi nghi ngờ bất kỳ C ++ lập trình viên sẽ tranh chấp đó. Nhưng không dễ thay đổi. ;)
jalf

10
Đây là quan điểm của Stroustrup về vấn đề này: groups.google.com/group/comp.lang.c++.moderated/msg/…
sbi

3
@sbi: Dù sao, +1 trên nhận xét của bạn chỉ để nghiên cứu lịch sử. Tôi tin rằng quan điểm của tác giả (B. Stroustrup) về tên của một khái niệm (RAII) là đủ thú vị để có câu trả lời cho riêng nó.
paercebal

1
@paercebal: Nghiên cứu lịch sử? Bây giờ bạn đã làm cho tôi cảm thấy rất già. :(Tôi đã đọc toàn bộ chủ đề, vào thời điểm đó, và thậm chí không coi mình là một người mới C ++!
sbi

3
+1, tôi cũng định hỏi câu hỏi tương tự, rất vui vì tôi không phải là người duy nhất hiểu khái niệm nhưng không hiểu tên. Có vẻ như nó nên được gọi là RAOI - Thu thập tài nguyên khi khởi tạo.
đoạt giải thưởng

Câu trả lời:


132

Vậy tại sao điều đó không được gọi là "sử dụng ngăn xếp để kích hoạt dọn dẹp" (UTSTTC :)?

RAII đang cho bạn biết phải làm gì: Nhận tài nguyên của bạn trong một hàm tạo! Tôi sẽ thêm: một tài nguyên, một hàm tạo. UTSTTC chỉ là một ứng dụng trong số đó, RAII còn nhiều hơn thế nữa.

Quản lý tài nguyên tệ quá. Ở đây, tài nguyên là bất cứ thứ gì cần dọn dẹp sau khi sử dụng. Các nghiên cứu về các dự án trên nhiều nền tảng cho thấy phần lớn các lỗi liên quan đến quản lý tài nguyên - và nó đặc biệt tồi tệ trên Windows (do có nhiều loại đối tượng và trình phân bổ).

Trong C ++, quản lý tài nguyên đặc biệt phức tạp do sự kết hợp của các mẫu ngoại lệ và (kiểu C ++). Để biết thêm chi tiết, hãy xem GOTW8 ).


C ++ đảm bảo rằng hàm hủy được gọi nếu và chỉ khi hàm tạo thành công. Dựa vào đó, RAII có thể giải quyết nhiều vấn đề khó chịu mà các lập trình viên bình thường thậm chí có thể không nhận thức được. Dưới đây là một vài ví dụ ngoài "biến cục bộ của tôi sẽ bị hủy bất cứ khi nào tôi quay lại".

Chúng ta hãy bắt đầu với một FileHandlelớp quá đơn giản sử dụng 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;
    }
}

Nếu quá trình xây dựng không thành công (với một ngoại lệ), không có hàm thành viên nào khác - thậm chí không phải hàm hủy - được gọi.

RAII tránh sử dụng các đối tượng ở trạng thái không hợp lệ. nó đã làm cho cuộc sống dễ dàng hơn trước cả khi chúng ta sử dụng đối tượng.

Bây giờ, chúng ta hãy xem xét các đối tượng tạm thời:

void CopyFileData(FileHandle source, FileHandle dest);

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

Có ba trường hợp lỗi cần xử lý: không mở được tệp, chỉ mở được một tệp, mở được cả hai tệp nhưng không sao chép được tệp. Trong một triển khai không RAII, Foosẽ phải xử lý cả ba trường hợp một cách rõ ràng.

RAII giải phóng các tài nguyên đã có được, ngay cả khi có nhiều tài nguyên trong một câu lệnh.

Bây giờ, chúng ta hãy tổng hợp một số đối tượng:

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!";
    }
}

Hàm khởi tạo của Loggersẽ bị lỗi nếu hàm originalkhởi tạo của không thành công (vì filename1không thể mở được), hàm tạo duplexcủa không thành công (vì filename2không thể mở được) hoặc việc ghi vào tệp bên trong Loggerthân hàm tạo của không thành công. Trong bất kỳ trường hợp nào, trình Loggerhủy của 's sẽ không được gọi - vì vậy chúng ta không thể dựa vào trình Loggerhủy của' để giải phóng tệp. Nhưng nếu originalđược xây dựng, hàm hủy của nó sẽ được gọi trong quá trình dọn dẹp hàm Loggertạo.

RAII đơn giản hóa việc dọn dẹp sau khi xây dựng một phần.


Điểm tiêu cực:

Điểm tiêu cực? Tất cả các vấn đề có thể được giải quyết với RAII và con trỏ thông minh ;-)

RAII đôi khi khó sử dụng khi bạn cần chuyển đổi chậm trễ, đẩy các đối tượng tổng hợp lên đống.
Hãy tưởng tượng Logger cần a SetTargetFile(const char* target). Trong trường hợp đó, tay cầm, vẫn cần là thành viên của Logger, cần nằm trên đống (ví dụ: trong một con trỏ thông minh, để kích hoạt việc phá hủy tay cầm một cách thích hợp.)

Tôi thực sự chưa bao giờ ước được thu gom rác. Khi tôi làm C #, đôi khi tôi cảm thấy một khoảnh khắc hạnh phúc mà tôi không cần phải quan tâm, nhưng nhiều hơn nữa tôi nhớ tất cả những món đồ chơi thú vị có thể được tạo ra thông qua sự phá hủy xác định. (sử dụng IDisposablechỉ không cắt nó.)

Tôi đã có một cấu trúc đặc biệt phức tạp có thể đã được hưởng lợi từ GC, nơi các con trỏ thông minh "đơn giản" sẽ gây ra các tham chiếu vòng tròn trên nhiều lớp. Chúng tôi đã cố gắng cân bằng cẩn thận các điểm mạnh và yếu, nhưng bất cứ khi nào chúng tôi muốn thay đổi điều gì đó, chúng tôi phải nghiên cứu một biểu đồ mối quan hệ lớn. GC có thể tốt hơn, nhưng một số thành phần có tài nguyên sẽ được phát hành càng sớm càng tốt.


Một lưu ý về mẫu FileHandle: Nó không được dự định là hoàn chỉnh, chỉ là một mẫu - nhưng hóa ra không chính xác. Cảm ơn Johannes Schaub đã chỉ ra và FredOverflow đã biến nó thành một giải pháp C ++ 0x chính xác. Theo thời gian, tôi đã ổn định với cách tiếp cận được ghi lại ở đây .


1
+1 Để trỏ rằng GC và ASAP không kết hợp. Không bị tổn thương thường nhưng khi nó có phải nó không phải dễ dàng để chẩn đoán: /
Matthieu M.

10
Một câu cụ thể mà tôi đã bỏ qua trong các bài đọc trước đó. Bạn nói rằng "RAII" đang nói với bạn, "Thu thập tài nguyên của bạn bên trong các trình xây dựng." Điều đó có ý nghĩa và gần như là một cách diễn giải từng chữ của "RAII". Bây giờ tôi thậm chí còn hiểu nó tốt hơn (Tôi sẽ bầu chọn bạn một lần nữa nếu tôi có thể :)
Charlie Flowers

2
Một ưu điểm chính của GC là khung cấp phát bộ nhớ có thể ngăn chặn việc tạo ra các tham chiếu lơ lửng trong trường hợp không có mã "không an toàn" (nếu mã "không an toàn" được cho phép, tất nhiên, khung không thể ngăn chặn bất cứ điều gì). GC cũng thường vượt trội hơn RAII khi xử lý các đối tượng bất biến được chia sẻ như chuỗi thường không có chủ sở hữu rõ ràng và không yêu cầu dọn dẹp. Thật không may khi nhiều khung công tác hơn không tìm cách kết hợp GC và RAII, vì hầu hết các ứng dụng sẽ có sự kết hợp giữa các đối tượng bất biến (nơi GC sẽ là tốt nhất) và các đối tượng cần dọn dẹp (nơi RAII là tốt nhất).
supercat

@supercat: Nói chung là tôi thích GC - nhưng nó chỉ hoạt động với những nguồn cấp lại mà GC "hiểu". Ví dụ, .NET GC không biết chi phí của các đối tượng COM. Khi chỉ cần tạo và hủy chúng trong một vòng lặp, nó sẽ vui vẻ cho phép ứng dụng chạy sâu vào không gian địa chỉ hoặc bộ nhớ ảo - bất cứ điều gì đến trước - mà không cần nghĩ đến việc có thể thực hiện GC. --- hơn nữa, ngay cả trong một môi trường GC'd hoàn hảo, tôi vẫn nhớ sức mạnh của sự phá hủy xác định: bạn có thể áp dụng cùng một mẫu cho các công cụ khác, ví dụ như hiển thị các phần tử giao diện người dùng trong điều kiện xác thực.
peterchen

@peterchen: Một điều tôi nghĩ là không có trong rất nhiều suy nghĩ liên quan đến OOP là khái niệm quyền sở hữu đối tượng. Theo dõi quyền sở hữu rõ ràng là cần thiết đối với các đối tượng có tài nguyên, nhưng cũng thường cần thiết đối với các đối tượng có thể thay đổi mà không có tài nguyên. Nói chung, các đối tượng nên đóng gói trạng thái có thể thay đổi của chúng trong các tham chiếu đến các đối tượng bất biến có thể được chia sẻ hoặc trong các đối tượng có thể thay đổi mà chúng là chủ sở hữu độc quyền. Quyền sở hữu độc quyền như vậy không nhất thiết ngụ ý quyền truy cập ghi độc quyền, nhưng nếu Foosở hữu Barvà thay Bozđổi nó, ...
supercat

42

Có những câu trả lời tuyệt vời ở đó, vì vậy tôi chỉ thêm một số điều đã quên.

0. RAII là về phạm vi

RAII là về cả hai:

  1. có được một tài nguyên (bất kể là tài nguyên nào) trong hàm tạo và hủy lấy nó trong hàm hủy.
  2. có hàm tạo được thực thi khi biến được khai báo và hàm hủy tự động thực thi khi biến vượt ra khỏi phạm vi.

Những người khác đã trả lời về điều đó, vì vậy tôi sẽ không nói chi tiết.

1. Khi viết mã bằng Java hoặc C #, bạn đã sử dụng RAII ...

MONSIEUR JOURDAIN: Cái gì! Khi tôi nói, "Nicole, mang dép cho tôi và đưa cho tôi mũ đi ngủ", đó là văn xuôi?

BÁC SĨ TRIẾT HỌC: Vâng, thưa ông.

MONSIEUR JOURDAIN: Trong hơn bốn mươi năm, tôi đã nói văn xuôi mà không biết gì về nó, và tôi rất biết ơn các bạn vì đã dạy tôi điều đó.

- Molière: Quý ông trung lưu, Màn 2, Cảnh 4

Như Monsieur Jourdain đã làm với văn xuôi, C # và thậm chí cả người Java đã sử dụng RAII, nhưng theo những cách ẩn. Ví dụ: mã Java sau (được viết theo cùng một cách trong C # bằng cách thay thế synchronizedbằng lock):

void foo()
{
   // etc.

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

   // etc.
}

... đã sử dụng RAII: Việc mua lại mutex được thực hiện trong từ khóa ( synchronizedhoặc lock) và việc hủy chuyển đổi sẽ được thực hiện khi thoát khỏi phạm vi.

Nó rất tự nhiên trong ký hiệu của nó, nó gần như không cần giải thích ngay cả đối với những người chưa bao giờ nghe nói về RAII.

Lợi thế của C ++ so với Java và C # ở đây là mọi thứ đều có thể được thực hiện bằng RAII. Ví dụ, không có trực tiếp xây dựng-in tương đương synchronizedcũng không locktrong C ++, nhưng chúng ta vẫn có thể có họ.

Trong C ++, nó sẽ được viết:

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.
}

có thể dễ dàng viết theo cách Java / C # (sử dụng macro C ++):

void foo()
{
   // etc.

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

   // etc.
}

2. RAII có cách sử dụng thay thế

TRẮNG RABBIT: [hát] Tôi đến muộn / Tôi đến muộn / Cho một ngày rất quan trọng. / Không có thời gian để nói "Xin chào." / Tạm biệt. / Tôi đến muộn, tôi đến muộn, tôi đến muộn.

- Alice ở xứ sở thần tiên (phiên bản Disney, 1951)

Bạn biết khi nào hàm tạo sẽ được gọi (khi khai báo đối tượng), và bạn biết khi nào hàm hủy tương ứng của nó sẽ được gọi (khi thoát khỏi phạm vi), vì vậy bạn có thể viết mã gần như kỳ diệu chỉ với một dòng. Chào mừng bạn đến với thế giới thần tiên của C ++ (ít nhất là theo quan điểm của nhà phát triển C ++).

Ví dụ: bạn có thể viết một đối tượng counter (tôi để đó như một bài tập) và sử dụng nó chỉ bằng cách khai báo biến của nó, giống như đối tượng lock ở trên đã được sử dụng:

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
}

tất nhiên, có thể được viết lại theo cách Java / C # bằng cách sử dụng macro:

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. Tại sao thiếu C ++ finally?

[SHOUTING] Đây là lần đếm ngược cuối cùng !

- Châu Âu: Đếm ngược cuối cùng (xin lỗi, tôi đã hết trích dẫn, ở đây ... :-)

Các finallykhoản được sử dụng trong C # / Java để xử lý thanh lý tài nguyên trong trường hợp xuất cảnh phạm vi (hoặc thông qua một returnhoặc một ngoại lệ ném).

Người đọc đặc tả tinh tế sẽ nhận thấy C ++ không có mệnh đề cuối cùng. Và đây không phải là một lỗi, vì C ++ không cần nó, vì RAII đã xử lý việc loại bỏ tài nguyên. (Và tin tôi đi, viết một hàm hủy trong C ++ dễ dàng hơn rất nhiều so với viết mệnh đề cuối cùng của Java đúng, hoặc thậm chí là một phương thức Dispose đúng của C #).

Tuy nhiên, đôi khi, một finallyđiều khoản sẽ rất hay. Chúng ta có thể làm điều đó trong C ++ không? Có, chúng tôi có thể! Và một lần nữa với việc sử dụng RAII thay thế.

Kết luận: RAII không chỉ là triết lý trong C ++: Đó là C ++

RAII? ĐÂY LÀ C ++ !!!

- Nhận xét phẫn nộ của nhà phát triển C ++, được sao chép một cách đáng xấu hổ bởi một vị vua Sparta khó hiểu và 300 người bạn của ông ta

Khi bạn đạt đến một số kinh nghiệm trong C ++, bạn bắt đầu suy nghĩ về RAII , về trình tạo và trình hủy thực thi tự động .

Bạn bắt đầu suy nghĩ về phạm vi , {}ký tự và trở thành những ký tự quan trọng nhất trong mã của bạn.

Và hầu hết mọi thứ đều phù hợp với RAII: an toàn ngoại lệ, mutexes, kết nối cơ sở dữ liệu, yêu cầu cơ sở dữ liệu, kết nối máy chủ, đồng hồ, tay cầm hệ điều hành, v.v. và cuối cùng, nhưng không kém phần quan trọng, bộ nhớ.

Phần cơ sở dữ liệu không phải là không đáng kể, như nếu bạn chấp nhận trả giá, bạn thậm chí có thể viết theo kiểu " lập trình giao dịch ", thực thi các dòng và các dòng mã cho đến khi quyết định, cuối cùng, nếu bạn muốn cam kết tất cả các thay đổi. hoặc, nếu không thể, hoàn nguyên tất cả các thay đổi (miễn là mỗi dòng đáp ứng ít nhất Bảo đảm Ngoại lệ Mạnh mẽ). (xem phần thứ hai của bài viết Herb's Sutter này về lập trình giao dịch).

Và giống như một câu đố, mọi thứ đều khớp.

RAII là một phần của C ++, C ++ không thể là C ++ nếu không có nó.

Điều này giải thích tại sao các nhà phát triển C ++ có kinh nghiệm lại say mê RAII và tại sao RAII là thứ đầu tiên họ tìm kiếm khi thử một ngôn ngữ khác.

Và nó giải thích tại sao Garbage Collector, trong khi bản thân nó là một phần công nghệ tuyệt vời, lại không quá ấn tượng theo quan điểm của nhà phát triển C ++:

  • RAII đã xử lý hầu hết các trường hợp do GC xử lý
  • GC xử lý tốt hơn RAII với các tham chiếu vòng tròn trên các đối tượng được quản lý thuần túy (giảm nhẹ bằng cách sử dụng thông minh con trỏ yếu)
  • Tuy nhiên, một GC bị giới hạn về bộ nhớ, trong khi RAII có thể xử lý bất kỳ loại tài nguyên nào.
  • Như đã mô tả ở trên, RAII có thể làm được nhiều hơn thế ...

Một người hâm mộ Java: Tôi muốn nói rằng GC hữu ích hơn RAII vì nó xử lý tất cả bộ nhớ và giải phóng bạn khỏi nhiều lỗi tiềm ẩn. Với GC, bạn có thể tạo các tham chiếu vòng tròn, trả về và lưu trữ các tham chiếu và rất khó để làm sai (lưu trữ một tham chiếu đến một đối tượng được cho là có tuổi thọ ngắn sẽ kéo dài thời gian tồn tại của nó, đây là một loại rò rỉ bộ nhớ, nhưng đó là vấn đề duy nhất) . Việc xử lý tài nguyên bằng GC không hoạt động, nhưng hầu hết các tài nguyên trong một ứng dụng đều có chu kỳ hoạt động nhỏ và một số ít tài nguyên còn lại không có vấn đề gì lớn. Tôi ước chúng ta có thể có cả GC và RAII, nhưng điều đó dường như là không thể.
maaartinus

16

1
Một số trong số đó phù hợp với câu hỏi của tôi, nhưng tìm kiếm đã không hiển thị chúng, cũng như danh sách "câu hỏi liên quan" xuất hiện sau khi bạn nhập câu hỏi mới. Cảm ơn vì các liên kết.
Charlie Flowers

1
@Charlie: Theo một số cách, bản dựng trong tìm kiếm rất yếu. Sử dụng cú pháp thẻ ( "[chủ đề]") là rất hữu ích, và nhiều người sử dụng google ...
dmckee --- cựu điều hành kitten

10

RAII đang sử dụng ngữ nghĩa hàm hủy C ++ để quản lý tài nguyên. Ví dụ, hãy xem xét một con trỏ thông minh. Bạn có một phương thức khởi tạo được tham số hóa của con trỏ khởi tạo con trỏ này với địa chỉ của đối tượng. Bạn phân bổ một con trỏ trên ngăn xếp:

SmartPointer pointer( new ObjectClass() );

Khi con trỏ thông minh ra khỏi phạm vi, trình hủy của lớp con trỏ sẽ xóa đối tượng được kết nối. Con trỏ được cấp phát ngăn xếp và đối tượng - được cấp phát đống.

Có một số trường hợp RAII không giúp được gì. Ví dụ: nếu bạn sử dụng con trỏ thông minh đếm tham chiếu (như boost :: shared_ptr) và tạo một cấu trúc giống như đồ thị với một chu trình, bạn có nguy cơ phải đối mặt với rò rỉ bộ nhớ vì các đối tượng trong một chu trình sẽ ngăn không cho nhau được giải phóng. Thu gom rác sẽ giúp chống lại điều này.


2
Vì vậy, nó nên được gọi là UCDSTMR :)
Daniel Daranas

Suy nghĩ lại, tôi nghĩ UDSTMR thích hợp hơn. Ngôn ngữ (C ++) được cung cấp, vì vậy không cần ký tự "C" trong từ viết tắt. UDSTMR là viết tắt của Use Destructor Semantics To Manage Resources.
Daniel Daranas

9

Tôi muốn nhấn mạnh hơn một chút so với các phản hồi trước đó.

RAII, Resource Acquisition Is Initialization có nghĩa là tất cả các tài nguyên có được phải được mua trong bối cảnh khởi tạo một đối tượng. Điều này cấm thu nhận tài nguyên "trần trụi". Cơ sở lý luận là dọn dẹp trong C ++ hoạt động trên cơ sở đối tượng, không phải cơ sở gọi hàm. Do đó, tất cả việc dọn dẹp phải được thực hiện bởi các đối tượng, không phải các lệnh gọi hàm. Theo nghĩa này, C ++ là hướng đối tượng nhiều hơn, ví dụ như Java. Dọn dẹp Java dựa trên lời gọi hàm trong finallycác mệnh đề.


Câu trả lời chính xác. Và "khởi tạo một đối tượng" có nghĩa là "các trình xây dựng", phải không?
Charlie Flowers

@Charlie: vâng, đặc biệt là trong trường hợp này.
MSalters

8

Tôi đồng tình với cpitis. Nhưng muốn nói thêm rằng tài nguyên có thể là bất cứ thứ gì không chỉ là bộ nhớ. Tài nguyên có thể là một tệp, một phần quan trọng, một chuỗi hoặc một kết nối cơ sở dữ liệu.

Nó được gọi là Resource Acquisition Is Initialization vì tài nguyên được lấy khi đối tượng điều khiển tài nguyên được xây dựng, Nếu phương thức khởi tạo bị lỗi (tức là do một ngoại lệ) thì tài nguyên đó không được lấy. Sau đó, khi đối tượng vượt ra khỏi phạm vi, tài nguyên sẽ được giải phóng. c ++ đảm bảo rằng tất cả các đối tượng trên ngăn xếp đã được xây dựng thành công sẽ bị hủy (điều này bao gồm các hàm tạo của các lớp cơ sở và các thành viên ngay cả khi hàm tạo siêu lớp không thành công).

Lý do đằng sau RAII là đảm bảo an toàn cho việc thu hồi tài nguyên ngoại lệ. Rằng tất cả các tài nguyên có được đều được giải phóng đúng cách bất kể trường hợp ngoại lệ xảy ra. Tuy nhiên, điều này phụ thuộc vào chất lượng của lớp có được tài nguyên (điều này phải là ngoại lệ an toàn và điều này là khó).


Tuyệt vời, cảm ơn bạn đã giải thích lý do đằng sau cái tên. Theo tôi hiểu, bạn có thể diễn giải RAII là "Đừng bao giờ có được bất kỳ tài nguyên nào thông qua bất kỳ cơ chế nào khác ngoài việc khởi tạo (dựa trên phương thức khởi tạo)". Đúng?
Charlie Flowers

Vâng, đây là chính sách của tôi, tuy nhiên tôi rất cảnh giác khi viết các lớp RAII của riêng mình vì chúng phải an toàn ngoại lệ. Khi tôi viết chúng, tôi cố gắng đảm bảo an toàn ngoại lệ bằng cách sử dụng lại các lớp RAII khác do các chuyên gia viết.
iain

Tôi không thấy chúng khó viết. Nếu các lớp học của bạn đủ nhỏ, chúng không khó chút nào.
Rob K

7

Vấn đề với việc thu gom rác là bạn mất đi khả năng phá hủy xác định, điều quan trọng đối với RAII. Khi một biến vượt ra ngoài phạm vi, nó phụ thuộc vào bộ thu gom rác khi nào đối tượng sẽ được lấy lại. Tài nguyên do đối tượng nắm giữ sẽ tiếp tục được giữ cho đến khi trình hủy được gọi.


4
Vấn đề không chỉ là thuyết tất định. Vấn đề thực sự là các trình hoàn thiện (đặt tên java) cản trở GC. GC hiệu quả vì nó không gọi lại các đối tượng đã chết, mà bỏ qua chúng vào quên lãng. Tổng công ty phải theo dõi đối tượng với finalizers theo một cách khác nhau để đảm bảo rằng chúng được gọi là
David Rodríguez - dribeas

1
ngoại trừ trong java / c #, bạn có thể dọn dẹp trong một khối cuối cùng hơn là trong một trình hoàn thiện.
jk.

4

RAII xuất phát từ Phân bổ Tài nguyên Là Khởi tạo. Về cơ bản, nó có nghĩa là khi một hàm tạo kết thúc quá trình thực thi, đối tượng được xây dựng sẽ được khởi tạo hoàn toàn và sẵn sàng sử dụng. Nó cũng ngụ ý rằng trình hủy sẽ giải phóng bất kỳ tài nguyên nào (ví dụ: bộ nhớ, tài nguyên hệ điều hành) thuộc sở hữu của đối tượng.

So với các ngôn ngữ / công nghệ được thu gom rác (ví dụ như Java, .NET), C ++ cho phép toàn quyền kiểm soát vòng đời của một đối tượng. Đối với một đối tượng được phân bổ ngăn xếp, bạn sẽ biết khi nào trình hủy của đối tượng sẽ được gọi (khi việc thực thi vượt ra khỏi phạm vi), điều không thực sự được kiểm soát trong trường hợp thu gom rác. Ngay cả khi sử dụng con trỏ thông minh trong C ++ (ví dụ: boost :: shared_ptr), bạn sẽ biết rằng khi không có tham chiếu đến đối tượng trỏ, hàm hủy của đối tượng đó sẽ được gọi.


3

Và làm thế nào bạn có thể tạo ra một thứ gì đó trên ngăn xếp để dọn dẹp thứ gì đó sống trên đống?

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.

Khi một thể hiện của int_buffer ra đời, nó phải có kích thước và nó sẽ cấp phát bộ nhớ cần thiết. Khi nó vượt ra khỏi phạm vi, nó sẽ được gọi là hàm hủy. Điều này rất hữu ích cho những thứ như các đối tượng đồng bộ hóa. Xem xét

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.

Ngoài ra, có trường hợp nào bạn không thể sử dụng RAII không?

Không thật sự lắm.

Bạn có bao giờ thấy mình ước ao được thu gom rác không? Ít nhất bạn có thể sử dụng một bộ thu gom rác cho một số đối tượng trong khi để những người khác được quản lý?

Không bao giờ. Việc thu gom rác chỉ giải quyết một tập hợp con rất nhỏ của quản lý tài nguyên động.


Tôi đã sử dụng Java và C # rất ít, vì vậy tôi chưa bao giờ bỏ lỡ nó, nhưng GC chắc chắn đã hạn chế phong cách của tôi khi nói đến quản lý tài nguyên khi tôi phải sử dụng chúng, vì tôi không thể sử dụng RAII.
Rob K

1
Tôi đã sử dụng C # rất nhiều và đồng ý với bạn 100%. Trên thực tế, tôi coi GC không xác định là một trách nhiệm trong một ngôn ngữ.
Nemanja Trifunovic

2

Đã có rất nhiều câu trả lời hay ở đây, nhưng tôi chỉ muốn nói thêm:
Một lời giải thích đơn giản về RAII là, trong C ++, một đối tượng được phân bổ trên ngăn xếp sẽ bị phá hủy bất cứ khi nào nó vượt ra khỏi phạm vi. Điều đó có nghĩa là, một trình hủy đối tượng sẽ được gọi và có thể thực hiện mọi thao tác dọn dẹp cần thiết.
Điều đó có nghĩa là, nếu một đối tượng được tạo mà không có "mới", thì không cần "xóa". Và đây cũng là ý tưởng đằng sau "con trỏ thông minh" - chúng nằm trên ngăn xếp, và về cơ bản bao bọc một đối tượng dựa trên đống.


1
Không, họ không. Nhưng bạn có lý do chính đáng để tạo một con trỏ thông minh trên heap không? Nhân tiện, con trỏ thông minh chỉ là một ví dụ về nơi RAII có thể hữu ích.
E Dominique

1
Có thể việc sử dụng "ngăn xếp" so với "đống" của tôi hơi cẩu thả - bởi một đối tượng trên "ngăn xếp" tôi có nghĩa là bất kỳ đối tượng cục bộ nào. Nó tự nhiên có thể là một phần của đối tượng, ví dụ trên heap. Bằng cách "tạo một con trỏ thông minh trên heap", tôi muốn sử dụng mới / xóa trên chính con trỏ thông minh.
E Dominique

1

RAII là từ viết tắt của Resource Acquisition Is Initialization.

Kỹ thuật này rất độc đáo đối với C ++ vì sự hỗ trợ của chúng cho cả hàm tạo & hủy & gần như tự động các hàm tạo khớp với các đối số được truyền vào hoặc trong trường hợp xấu nhất, hàm tạo mặc định được gọi là & hàm hủy nếu sự rõ ràng được cung cấp được gọi khác là mặc định được thêm bởi trình biên dịch C ++ được gọi nếu bạn không viết hàm hủy rõ ràng cho một lớp C ++. Điều này chỉ xảy ra đối với các đối tượng C ++ được tự động quản lý - nghĩa là không sử dụng kho lưu trữ miễn phí (bộ nhớ được cấp phát / phân bổ bằng cách sử dụng các toán tử C ++ mới, mới [] / delete, delete []).

Kỹ thuật RAII sử dụng tính năng đối tượng được quản lý tự động này để xử lý các đối tượng được tạo trên heap / free-store bằng cách yêu cầu thêm bộ nhớ bằng cách sử dụng new / new [], sẽ bị hủy một cách rõ ràng bằng cách gọi delete / delete [] . Lớp của đối tượng được quản lý tự động sẽ bọc đối tượng này một đối tượng khác được tạo trên heap / bộ nhớ lưu trữ miễn phí. Do đó, khi phương thức khởi tạo của đối tượng được quản lý tự động chạy, đối tượng được bọc sẽ được tạo trên heap / bộ nhớ lưu trữ miễn phí và khi xử lý của đối tượng được quản lý tự động vượt ra ngoài phạm vi, hàm hủy của đối tượng được quản lý tự động đó được gọi tự động trong đó gói đối tượng bị hủy bằng cách sử dụng xóa. Với các khái niệm OOP, nếu bạn bọc các đối tượng như vậy bên trong một lớp khác trong phạm vi riêng tư, bạn sẽ không có quyền truy cập vào các thành viên và phương thức của lớp được bọc & đây là lý do tại sao con trỏ thông minh (hay còn gọi là các lớp xử lý) được thiết kế cho. Các con trỏ thông minh này hiển thị đối tượng được bao bọc dưới dạng đối tượng được đánh máy với thế giới bên ngoài & ở đó bằng cách cho phép gọi bất kỳ thành viên / phương thức nào mà đối tượng bộ nhớ tiếp xúc được tạo thành. Lưu ý rằng con trỏ thông minh có nhiều hương vị khác nhau dựa trên các nhu cầu khác nhau. Bạn nên tham khảo Lập trình C ++ Hiện đại của Andrei Alexandrescu hoặc tài liệu / triển khai shared_ptr.hpp của thư viện boost (www.boostorg) để tìm hiểu thêm về nó. Hy vọng điều này sẽ giúp bạn hiểu RAII. Bạn nên tham khảo Lập trình C ++ Hiện đại của Andrei Alexandrescu hoặc tài liệu / triển khai shared_ptr.hpp của thư viện boost (www.boostorg) để tìm hiểu thêm về nó. Hy vọng điều này sẽ giúp bạn hiểu RAII. Bạn nên tham khảo Lập trình C ++ Hiện đại của Andrei Alexandrescu hoặc tài liệu / triển khai shared_ptr.hpp của thư viện boost (www.boostorg) để tìm hiểu thêm về nó. Hy vọng điều này sẽ giúp bạn hiểu RAII.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.