C ++ có hỗ trợ các khối 'cuối cùng' không? (Và đây là 'RAII' mà tôi vẫn nghe về?)


Câu trả lời:


273

Không, C ++ không hỗ trợ các khối 'cuối cùng'. Lý do là C ++ thay vì hỗ trợ RAII: "Resource Acquisition Is khởi" - một tên nghèo cho một khái niệm thực sự hữu ích.

Ý tưởng là hàm hủy của đối tượng chịu trách nhiệm giải phóng tài nguyên. Khi đối tượng có thời lượng lưu trữ tự động, hàm hủy của đối tượng sẽ được gọi khi khối được tạo ra thoát - ngay cả khi khối đó được thoát khi có ngoại lệ. Dưới đây là lời giải thích của Bjarne Stroustrup về chủ đề này.

Một cách sử dụng phổ biến cho RAII là khóa một mutex:

// A class with implements RAII
class lock
{
    mutex &m_;

public:
    lock(mutex &m)
      : m_(m)
    {
        m.acquire();
    }
    ~lock()
    {
        m_.release();
    }
};

// A class which uses 'mutex' and 'lock' objects
class foo
{
    mutex mutex_; // mutex for locking 'foo' object
public:
    void bar()
    {
        lock scopeLock(mutex_); // lock object.

        foobar(); // an operation which may throw an exception

        // scopeLock will be destructed even if an exception
        // occurs, which will release the mutex and allow
        // other functions to lock the object and run.
    }
};

RAII cũng đơn giản hóa việc sử dụng các đối tượng như là thành viên của các lớp khác. Khi lớp sở hữu 'bị hủy, tài nguyên được quản lý bởi lớp RAII sẽ được giải phóng do hàm hủy cho lớp do RAII quản lý được gọi là kết quả. Điều này có nghĩa là khi bạn sử dụng RAII cho tất cả các thành viên trong một lớp quản lý tài nguyên, bạn có thể thoát khỏi việc sử dụng một hàm hủy rất đơn giản, thậm chí là mặc định cho lớp chủ sở hữu vì nó không cần phải quản lý vòng đời tài nguyên thành viên của nó một cách thủ công . (Cám ơn Mike B đã chỉ ra điều này.)

Đối với những familliar có C # hoặc VB.NET, bạn có thể nhận ra rằng RAII tương tự như phá hủy xác định .NET bằng cách sử dụng các câu lệnh IDis Dùng và 'sử dụng' . Thật vậy, hai phương pháp rất giống nhau. Sự khác biệt chính là RAII sẽ xác định rõ ràng bất kỳ loại tài nguyên nào - bao gồm cả bộ nhớ. Khi triển khai IDis Dùng trong .NET (ngay cả ngôn ngữ .NET C ++ / CLI), tài nguyên sẽ được giải phóng một cách xác định ngoại trừ bộ nhớ. Trong .NET, bộ nhớ không được phát hành một cách xác định; bộ nhớ chỉ được giải phóng trong chu kỳ thu gom rác.

 

Một số người tin rằng "Phá hủy là từ bỏ tài nguyên" là tên chính xác hơn cho thành ngữ RAII.


18
"Phá hủy là từ bỏ tài nguyên" - TRỰC TIẾP ... Không, không làm việc cho tôi. = P
Erik Forbes

14
RAII bị kẹt - thực sự không có gì thay đổi. Cố gắng làm như vậy sẽ là ngu ngốc. Tuy nhiên, bạn phải thừa nhận rằng "Thu nhận tài nguyên là khởi tạo" vẫn là một cái tên khá nghèo nàn.
Kevin

162
SBRM == Quản lý tài nguyên giới hạn phạm vi
Johannes Schaub - litb

10
Bất cứ ai có kỹ năng thiết kế không chỉ phần mềm nói chung, hãy để một mình các kỹ thuật được cải tiến, không thể đưa ra lý do xứng đáng cho một từ viết tắt khủng khiếp như vậy.
Hardryv

54
Điều này khiến bạn bị mắc kẹt khi bạn có thứ gì đó để dọn dẹp mà không phù hợp với bất kỳ cuộc sống của đối tượng C ++ nào. Tôi đoán bạn kết thúc với Trọn đời bằng C ++ Lớp học hoặc Else It Gets xấu xí (LECCLEOEIGU?).
Warren P

79

Trong C ++, cuối cùng là KHÔNG bắt buộc vì RAII.

RAII chuyển trách nhiệm về an toàn ngoại lệ từ người sử dụng đối tượng sang người thiết kế (và người thực hiện) đối tượng. Tôi sẽ tranh luận đây là nơi chính xác vì sau đó bạn chỉ cần lấy ngoại lệ an toàn chính xác một lần (trong thiết kế / thực hiện). Bằng cách sử dụng cuối cùng, bạn cần có được sự an toàn ngoại lệ chính xác mỗi khi bạn sử dụng một đối tượng.

Ngoài ra IMO mã trông gọn gàng hơn (xem bên dưới).

Thí dụ:

Một đối tượng cơ sở dữ liệu. Để đảm bảo kết nối DB được sử dụng, nó phải được mở và đóng. Bằng cách sử dụng RAII, điều này có thể được thực hiện trong hàm tạo / hàm hủy.

C ++ Giống như RAII

void someFunc()
{
    DB    db("DBDesciptionString");
    // Use the db object.

} // db goes out of scope and destructor closes the connection.
  // This happens even in the presence of exceptions.

Việc sử dụng RAII làm cho việc sử dụng một đối tượng DB chính xác rất dễ dàng. Đối tượng DB sẽ tự đóng chính xác bằng cách sử dụng hàm hủy cho dù chúng ta thử và lạm dụng nó như thế nào.

Cuối cùng thích Java

void someFunc()
{
    DB      db = new DB("DBDesciptionString");
    try
    {
        // Use the db object.
    }
    finally
    {
        // Can not rely on finaliser.
        // So we must explicitly close the connection.
        try
        {
            db.close();
        }
        catch(Throwable e)
        {
           /* Ignore */
           // Make sure not to throw exception if one is already propagating.
        }
    }
}

Khi sử dụng cuối cùng, việc sử dụng đúng đối tượng được ủy quyền cho người sử dụng đối tượng. tức là trách nhiệm của người dùng đối tượng là phải đóng chính xác kết nối DB. Bây giờ bạn có thể lập luận rằng điều này có thể được thực hiện trong bộ hoàn thiện, nhưng tài nguyên có thể có hạn chế hoặc các ràng buộc khác và do đó bạn thường muốn kiểm soát việc phát hành đối tượng và không dựa vào hành vi không xác định của người thu gom rác.

Ngoài ra đây là một ví dụ đơn giản.
Khi bạn có nhiều tài nguyên cần được phát hành, mã có thể trở nên phức tạp.

Một phân tích chi tiết hơn có thể được tìm thấy ở đây: http://accu.org/index.php/journals/236


16
// Make sure not to throw exception if one is already propagating.Điều quan trọng đối với các hàm hủy C ++ là không ném ngoại lệ cũng vì lý do này.
Cemafor

10
@Cemafor: Lý do để C ++ không ném ngoại lệ ra khỏi hàm hủy khác với Java. Trong Java nó sẽ hoạt động (bạn chỉ mất ngoại lệ ban đầu). Trong C ++ thì nó rất tệ. Nhưng điểm quan trọng trong C ++ là bạn chỉ phải thực hiện một lần (bởi người thiết kế lớp) khi anh ta viết hàm hủy. Trong Java, bạn phải làm điều đó tại điểm sử dụng. Vì vậy, trách nhiệm của người sử dụng lớp là viết cùng một tấm nồi hơi.
Martin York

1
Nếu đó là vấn đề "cần thiết", bạn cũng không cần RAII. Hãy thoát khỏi nó! :-) Đùa sang một bên, RAII là tốt cho rất nhiều trường hợp. Những gì RAII làm cho cồng kềnh hơn là các trường hợp khi bạn muốn thực thi một số mã (không liên quan đến tài nguyên) ngay cả khi mã ở trên được trả về sớm. Cho rằng bạn sử dụng gotos hoặc tách nó thành hai phương pháp.
Trinidad

1
@Trinidad: Nó không đơn giản như bạn nghĩ (vì tất cả các đề xuất của bạn dường như chọn các tùy chọn tồi tệ nhất có thể). Đó là lý do tại sao một câu hỏi có thể là một nơi tốt hơn để khám phá điều này hơn các ý kiến.
Martin York

1
Việc chỉ trích "KHÔNG bắt buộc vì RAII": có rất nhiều trường hợp khi thêm RAII ad-hoc sẽ có quá nhiều mã soạn sẵn để thêm, và cuối cùng sẽ cực kỳ thích hợp.
ceztko

63

RAII thường tốt hơn, nhưng bạn có thể dễ dàng có ngữ nghĩa cuối cùng trong C ++. Sử dụng một lượng nhỏ mã.

Bên cạnh đó, Nguyên tắc cốt lõi C ++ đưa ra cuối cùng.

Đây là liên kết đến triển khai Microsoft GSL và liên kết đến triển khai Martin Moene

Bjarne Stroustrup nhiều lần nói rằng tất cả mọi thứ trong GSL đều có nghĩa là đi theo tiêu chuẩn. Vì vậy, nó nên là một cách chứng minh trong tương lai để sử dụng cuối cùng .

Bạn có thể dễ dàng thực hiện chính mình nếu bạn muốn, tiếp tục đọc.

Trong C ++ 11 RAII và lambdas cho phép tạo ra một vị tướng cuối cùng:

namespace detail { //adapt to your "private" namespace
template <typename F>
struct FinalAction {
    FinalAction(F f) : clean_{f} {}
   ~FinalAction() { if(enabled_) clean_(); }
    void disable() { enabled_ = false; };
  private:
    F clean_;
    bool enabled_{true}; }; }

template <typename F>
detail::FinalAction<F> finally(F f) {
    return detail::FinalAction<F>(f); }

ví dụ về việc sử dụng:

#include <iostream>
int main() {
    int* a = new int;
    auto delete_a = finally([a] { delete a; std::cout << "leaving the block, deleting a!\n"; });
    std::cout << "doing something ...\n"; }

đầu ra sẽ là:

doing something...
leaving the block, deleting a!

Cá nhân tôi đã sử dụng điều này vài lần để đảm bảo đóng bộ mô tả tệp POSIX trong chương trình C ++.

Có một lớp thực sự quản lý tài nguyên và vì vậy tránh mọi loại rò rỉ thường tốt hơn, nhưng điều này cuối cùng cũng hữu ích trong trường hợp làm cho một lớp nghe có vẻ như quá mức cần thiết.

Ngoài ra, tôi thích nó hơn các ngôn ngữ khác cuối cùng vì nếu được sử dụng một cách tự nhiên, bạn viết mã đóng gần mã mở (trong ví dụ của tôi là mớixóa ) và hủy theo sau khi xây dựng theo thứ tự LIFO như bình thường trong C ++. Nhược điểm duy nhất là bạn nhận được một biến tự động mà bạn không thực sự sử dụng và cú pháp lambda làm cho nó hơi ồn (trong ví dụ của tôi ở dòng thứ tư chỉ có từ cuối cùng và {} -block bên phải là có ý nghĩa, phần còn lại về cơ bản là tiếng ồn).

Một vi dụ khac:

 [...]
 auto precision = std::cout.precision();
 auto set_precision_back = finally( [precision, &std::cout]() { std::cout << std::setprecision(precision); } );
 std::cout << std::setprecision(3);

Thành viên vô hiệu hóa là hữu ích nếu cuối cùng phải được gọi chỉ trong trường hợp thất bại. Ví dụ: bạn phải sao chép một đối tượng trong ba thùng chứa khác nhau, bạn có thể thiết lập cuối cùng để hoàn tác từng bản sao và vô hiệu hóa sau khi tất cả các bản sao thành công. Làm như vậy, nếu phá hủy không thể ném, bạn đảm bảo sự đảm bảo mạnh mẽ.

vô hiệu hóa ví dụ:

//strong guarantee
void copy_to_all(BIGobj const& a) {
    first_.push_back(a);
    auto undo_first_push = finally([first_&] { first_.pop_back(); });

    second_.push_back(a);
    auto undo_second_push = finally([second_&] { second_.pop_back(); });

    third_.push_back(a);
    //no necessary, put just to make easier to add containers in the future
    auto undo_third_push = finally([third_&] { third_.pop_back(); });

    undo_first_push.disable();
    undo_second_push.disable();
    undo_third_push.disable(); }

Nếu bạn không thể sử dụng C ++ 11 thì cuối cùng bạn vẫn có thể có , nhưng mã trở nên dài hơn một chút. Chỉ cần định nghĩa một cấu trúc chỉ có một hàm tạo và hàm hủy, hàm tạo sẽ tham chiếu đến bất cứ thứ gì cần thiết và hàm hủy thực hiện các hành động bạn cần. Đây là cơ bản những gì lambda làm, được thực hiện thủ công.

#include <iostream>
int main() {
    int* a = new int;

    struct Delete_a_t {
        Delete_a_t(int* p) : p_(p) {}
       ~Delete_a_t() { delete p_; std::cout << "leaving the block, deleting a!\n"; }
        int* p_;
    } delete_a(a);

    std::cout << "doing something ...\n"; }

Có thể có một vấn đề có thể xảy ra: trong hàm 'cuối cùng (F f)', nó trả về một đối tượng của FinalAction, do đó, bộ giải mã có thể được gọi trước khi trả lại hàm cuối cùng. Có lẽ chúng ta nên sử dụng hàm std :: thay vì mẫu F.
user1633272

Lưu ý rằng FinalActionvề cơ bản giống như ScopeGuardthành ngữ phổ biến , chỉ với một tên khác.
andera

1
Tối ưu hóa này có an toàn không?
Nulano

2
@ Paolo.Bolzoni Xin lỗi vì đã không trả lời sớm hơn, tôi đã không nhận được thông báo cho nhận xét của bạn. Tôi đã lo lắng khối cuối cùng (trong đó tôi gọi là hàm DLL) sẽ được gọi trước khi kết thúc phạm vi (vì biến này không được sử dụng), nhưng từ đó đã tìm thấy một câu hỏi về SO đã xóa tan nỗi lo của tôi. Tôi sẽ liên kết với nó, nhưng thật không may, tôi không thể tìm thấy nó nữa.
Nulano

1
Hàm vô hiệu hóa () là một loại mụn cóc trên thiết kế sạch khác của bạn. Nếu bạn muốn cuối cùng chỉ được gọi trong trường hợp thất bại, thì tại sao không sử dụng câu lệnh bắt? Đó không phải là những gì nó làm?
dùng2445507

32

Ngoài việc làm sạch dễ dàng với các đối tượng dựa trên ngăn xếp, RAII cũng hữu ích vì việc dọn dẹp 'tự động' tương tự xảy ra khi đối tượng là thành viên của một lớp khác. Khi lớp sở hữu bị hủy, tài nguyên được quản lý bởi lớp RAII sẽ bị xóa sạch do kết quả của dtor cho lớp đó được gọi là kết quả.

Điều này có nghĩa là khi bạn đạt RAII nirvana và tất cả các thành viên trong một lớp sử dụng RAII (như con trỏ thông minh), bạn có thể thoát khỏi một dtor rất đơn giản (thậm chí có thể mặc định) cho lớp chủ sở hữu vì nó không cần phải quản lý thủ công tuổi thọ tài nguyên thành viên.


Đó là một điểm rất tốt. +1 cho bạn. Không có nhiều người khác đã bỏ phiếu cho bạn mặc dù. Tôi hy vọng bạn không bận tâm rằng tôi đã chỉnh sửa bài đăng của mình để bao gồm các bình luận của bạn. (Tôi đã cho bạn tín dụng tất nhiên.) Cảm ơn! :)
Kevin

30

tại sao ngay cả các ngôn ngữ được quản lý cũng cung cấp một khối cuối cùng mặc dù các tài nguyên được tự động xử lý bởi trình thu gom rác?

Trên thực tế, các ngôn ngữ dựa trên người thu gom rác cần "cuối cùng" nhiều hơn. Trình thu gom rác không phá hủy các đối tượng của bạn một cách kịp thời, do đó không thể dựa vào đó để dọn sạch các vấn đề không liên quan đến bộ nhớ một cách chính xác.

Về mặt dữ liệu được phân bổ động, nhiều người sẽ cho rằng bạn nên sử dụng các con trỏ thông minh.

Tuy nhiên...

RAII chuyển trách nhiệm an toàn ngoại lệ từ người sử dụng đối tượng sang người thiết kế

Đáng buồn thay, đây là sự sụp đổ của chính nó. Thói quen lập trình C cũ chết cứng. Khi bạn đang sử dụng thư viện viết bằng chữ C hoặc kiểu rất C, RAII sẽ không được sử dụng. Viết lại toàn bộ giao diện API, đó chỉ là những gì bạn phải làm việc. Sau đó, thiếu "cuối cùng" thực sự cắn.


13
Chính xác ... RAII có vẻ tốt đẹp từ một quan điểm lý tưởng. Nhưng tôi phải làm việc với các API C thông thường mọi lúc (như các hàm kiểu C trong API Win32 ...). Rất phổ biến để có được một tài nguyên trả về một loại TAY, sau đó yêu cầu một số chức năng như CloseHandle (HANDLE) để dọn sạch. Sử dụng thử ... cuối cùng là một cách tốt để xử lý các trường hợp ngoại lệ có thể. (Rất may, có vẻ như shared_ptr với các thông số tùy chỉnh và lambdas C ++ 11 sẽ cung cấp một số cứu trợ dựa trên RAII không yêu cầu viết toàn bộ các lớp để bọc một số API tôi chỉ sử dụng ở một nơi.).
James Johnston

7
@JamesJohnston, thật dễ dàng để viết một lớp bao bọc chứa bất kỳ loại tay cầm nào và cung cấp cơ chế RAII. ATL cung cấp một loạt các ví dụ. Có vẻ như bạn coi điều này là quá nhiều rắc rối nhưng tôi không đồng ý, chúng rất nhỏ và dễ viết.
Đánh dấu tiền chuộc

5
Có đơn giản, nhỏ không. Kích thước phụ thuộc vào độ phức tạp của thư viện bạn đang làm việc.
Philip Couling

1
@MarkRansom: Có bất kỳ cơ chế nào mà RAII có thể làm điều gì đó thông minh nếu một ngoại lệ xảy ra trong khi dọn dẹp trong khi một ngoại lệ khác đang chờ xử lý? Trong các hệ thống có thử / cuối cùng, có thể - mặc dù lúng túng - sắp xếp mọi thứ sao cho ngoại lệ đang chờ xử lý và ngoại lệ xảy ra trong quá trình dọn dẹp đều được lưu trữ trong một cái mới CleanupFailedException. Có cách nào hợp lý để đạt được kết quả như vậy khi sử dụng RAII không?
supercat

3
@couling: Có nhiều trường hợp chương trình sẽ gọi một SomeObject.DoSomething()phương thức và muốn biết liệu nó (1) có thành công hay không, (2) không có tác dụng phụ , (3) không thành công với tác dụng phụ mà người gọi đã chuẩn bị để đối phó hoặc (4) không thành công với các tác dụng phụ mà người gọi không thể đối phó được. Chỉ người gọi sẽ biết những tình huống có thể và không thể đối phó với; những gì người gọi cần là một cách để biết tình hình là gì. Thật tệ khi không có cơ chế tiêu chuẩn để cung cấp thông tin quan trọng nhất về một ngoại lệ.
supercat

9

Một mô phỏng khối "cuối cùng" khác sử dụng các hàm lambda C ++ 11

template <typename TCode, typename TFinallyCode>
inline void with_finally(const TCode &code, const TFinallyCode &finally_code)
{
    try
    {
        code();
    }
    catch (...)
    {
        try
        {
            finally_code();
        }
        catch (...) // Maybe stupid check that finally_code mustn't throw.
        {
            std::terminate();
        }
        throw;
    }
    finally_code();
}

Hãy hy vọng trình biên dịch sẽ tối ưu hóa mã ở trên.

Bây giờ chúng ta có thể viết mã như thế này:

with_finally(
    [&]()
    {
        try
        {
            // Doing some stuff that may throw an exception
        }
        catch (const exception1 &)
        {
            // Handling first class of exceptions
        }
        catch (const exception2 &)
        {
            // Handling another class of exceptions
        }
        // Some classes of exceptions can be still unhandled
    },
    [&]() // finally
    {
        // This code will be executed in all three cases:
        //   1) exception was not thrown at all
        //   2) exception was handled by one of the "catch" blocks above
        //   3) exception was not handled by any of the "catch" block above
    }
);

Nếu bạn muốn bạn có thể gói thành ngữ này vào macro "thử - cuối cùng":

// Please never throw exception below. It is needed to avoid a compilation error
// in the case when we use "begin_try ... finally" without any "catch" block.
class never_thrown_exception {};

#define begin_try    with_finally([&](){ try
#define finally      catch(never_thrown_exception){throw;} },[&]()
#define end_try      ) // sorry for "pascalish" style :(

Bây giờ khối "cuối cùng" đã có sẵn trong C ++ 11:

begin_try
{
    // A code that may throw
}
catch (const some_exception &)
{
    // Handling some exceptions
}
finally
{
    // A code that is always executed
}
end_try; // Sorry again for this ugly thing

Cá nhân tôi không thích phiên bản "macro" của thành ngữ "cuối cùng" và thích sử dụng hàm "with_finally" thuần túy ngay cả khi một cú pháp cồng kềnh hơn trong trường hợp đó.

Bạn có thể kiểm tra mã ở trên tại đây: http://coliru.stacked-crooking.com/a/1d88f64cb27b3813

PS

Nếu bạn cần một khối cuối cùng trong mã của mình, thì các bộ bảo vệ có phạm vi hoặc các macro ON_FINALLY / ON_EXCEPTION có thể sẽ phù hợp hơn với nhu cầu của bạn.

Dưới đây là ví dụ ngắn về việc sử dụng ON_FINALLY / ON_EXCEPTION:

void function(std::vector<const char*> &vector)
{
    int *arr1 = (int*)malloc(800*sizeof(int));
    if (!arr1) { throw "cannot malloc arr1"; }
    ON_FINALLY({ free(arr1); });

    int *arr2 = (int*)malloc(900*sizeof(int));
    if (!arr2) { throw "cannot malloc arr2"; }
    ON_FINALLY({ free(arr2); });

    vector.push_back("good");
    ON_EXCEPTION({ vector.pop_back(); });

    ...

1
Đầu tiên là với tôi dễ đọc nhất trong số tất cả các tùy chọn được trình bày trong trang này. +1
Nikos

7

Xin lỗi vì đã đào một chủ đề cũ như vậy, nhưng có một lỗi lớn trong lý do sau:

RAII chuyển trách nhiệm về an toàn ngoại lệ từ người sử dụng đối tượng sang người thiết kế (và người thực hiện) đối tượng. Tôi sẽ tranh luận đây là nơi chính xác vì sau đó bạn chỉ cần lấy ngoại lệ an toàn chính xác một lần (trong thiết kế / thực hiện). Bằng cách sử dụng cuối cùng, bạn cần có được sự an toàn ngoại lệ chính xác mỗi khi bạn sử dụng một đối tượng.

Thường xuyên hơn không, bạn phải xử lý các đối tượng được phân bổ động, số lượng đối tượng động, v.v. Trong khối thử, một số mã có thể tạo ra nhiều đối tượng (có bao nhiêu đối tượng được xác định trong thời gian chạy) và lưu trữ con trỏ vào chúng trong danh sách. Bây giờ, đây không phải là một kịch bản kỳ lạ, nhưng rất phổ biến. Trong trường hợp này, bạn muốn viết những thứ như

void DoStuff(vector<string> input)
{
  list<Foo*> myList;

  try
  {    
    for (int i = 0; i < input.size(); ++i)
    {
      Foo* tmp = new Foo(input[i]);
      if (!tmp)
        throw;

      myList.push_back(tmp);
    }

    DoSomeStuff(myList);
  }
  finally
  {
    while (!myList.empty())
    {
      delete myList.back();
      myList.pop_back();
    }
  }
}

Tất nhiên, danh sách sẽ bị hủy khi đi ra khỏi phạm vi, nhưng điều đó sẽ không dọn sạch các đối tượng tạm thời mà bạn đã tạo.

Thay vào đó, bạn phải đi con đường xấu xí:

void DoStuff(vector<string> input)
{
  list<Foo*> myList;

  try
  {    
    for (int i = 0; i < input.size(); ++i)
    {
      Foo* tmp = new Foo(input[i]);
      if (!tmp)
        throw;

      myList.push_back(tmp);
    }

    DoSomeStuff(myList);
  }
  catch(...)
  {
  }

  while (!myList.empty())
  {
    delete myList.back();
    myList.pop_back();
  }
}

Ngoài ra: tại sao các làn đường được quản lý thậm chí còn cung cấp một khối cuối cùng mặc dù các tài nguyên được tự động xử lý bởi bộ thu gom rác?

Gợi ý: có nhiều thứ bạn có thể làm với "cuối cùng" hơn là chỉ giải quyết bộ nhớ.


17
Các ngôn ngữ được quản lý cần các khối cuối cùng chính xác vì chỉ có một loại tài nguyên được quản lý tự động: bộ nhớ. RAII có nghĩa là tất cả các tài nguyên có thể được xử lý theo cùng một cách, do đó cuối cùng không cần. Nếu bạn thực sự sử dụng RAII trong ví dụ của bạn (bằng cách sử dụng các con trỏ thông minh trong danh sách của bạn thay vì các mã trần), mã sẽ đơn giản hơn mẫu "cuối cùng" của bạn. Và thậm chí đơn giản hơn nếu bạn không kiểm tra giá trị trả về của cái mới - kiểm tra nó là khá vô nghĩa.
Myto

7
newkhông trả lại NULL, thay vào đó, nó ném một ngoại lệ
Hasturkun

5
Bạn đưa ra một câu hỏi quan trọng, nhưng nó có 2 câu trả lời có thể. Một là do Myto đưa ra - sử dụng các con trỏ thông minh cho tất cả các phân bổ động. Khác là sử dụng các thùng chứa tiêu chuẩn, luôn phá hủy nội dung của chúng khi phá hủy. Dù bằng cách nào, mọi đối tượng được phân bổ cuối cùng đều thuộc sở hữu của một đối tượng được phân bổ tĩnh tự động giải phóng nó khi bị phá hủy. Thật xấu hổ khi các giải pháp tốt hơn này rất khó để các lập trình viên khám phá vì khả năng hiển thị cao của các con trỏ và mảng đơn giản.
j_random_hacker

4
C ++ 11 cải thiện điều này và bao gồm std::shared_ptrstd::unique_ptrtrực tiếp trong stdlib.
u0b34a0f6ae

16
Lý do ví dụ của bạn rất tệ hại là vì RAII không hoàn hảo, thay vào đó là do bạn không sử dụng nó. Con trỏ thô không phải là RAII.
Ben Voigt

6

FWIW, Microsoft Visual C ++ không hỗ trợ thử, cuối cùng và nó đã được sử dụng trong các ứng dụng MFC như một phương pháp để bắt các trường hợp ngoại lệ nghiêm trọng có thể dẫn đến sự cố. Ví dụ;

int CMyApp::Run() 
{
    __try
    {
        int i = CWinApp::Run();
        m_Exitok = MAGIC_EXIT_NO;
        return i;
    }
    __finally
    {
        if (m_Exitok != MAGIC_EXIT_NO)
            FaultHandler();
    }
}

Tôi đã sử dụng điều này trong quá khứ để làm những việc như lưu bản sao lưu của các tệp đang mở trước khi thoát. Một số cài đặt gỡ lỗi JIT sẽ phá vỡ cơ chế này mặc dù.


4
hãy nhớ rằng đó không phải là ngoại lệ thực sự của C ++, mà là những trường hợp SEH. Bạn có thể sử dụng cả trong mã MS C ++. SEH là một trình xử lý ngoại lệ của hệ điều hành, đó là cách VB, .NET thực hiện các ngoại lệ.
gbjbaanb

và bạn có thể sử dụng SetUnhandledExceptionHandler để tạo một trình xử lý ngoại lệ không bắt được 'toàn cầu' - cho các ngoại lệ SEH.
gbjbaanb

3
SEH là khủng khiếp và cũng ngăn chặn kẻ hủy diệt C ++ được gọi
paulm

6

Như đã chỉ ra trong các câu trả lời khác, C ++ có thể hỗ trợ finallychức năng giống như. Việc triển khai chức năng này có lẽ là gần nhất với ngôn ngữ tiêu chuẩn là một trong các Nguyên tắc cốt lõi C ++ , một tập hợp các cách thực hành tốt nhất để sử dụng C ++ do Bjarne Stoustrup và Herb Sutter chỉnh sửa. Việc triển khaifinally là một phần của Thư viện Hỗ trợ Nguyên tắc (GSL). Trong suốt Hướng dẫn, finallykhuyến nghị sử dụng khi xử lý các giao diện kiểu cũ và nó cũng có một hướng dẫn riêng, có tiêu đề Sử dụng đối tượng Final_action để thể hiện dọn dẹp nếu không có xử lý tài nguyên phù hợp .

Vì vậy, không chỉ hỗ trợ C ++ finally, thực sự nên sử dụng nó trong rất nhiều trường hợp sử dụng phổ biến.

Một ví dụ sử dụng triển khai GSL sẽ như sau:

#include <gsl/gsl_util.h>

void example()
{
    int handle = get_some_resource();
    auto handle_clean = gsl::finally([&handle] { clean_that_resource(handle); });

    // Do a lot of stuff, return early and throw exceptions.
    // clean_that_resource will always get called.
}

Việc triển khai và sử dụng GSL rất giống với câu trả lời của Paolo.Bolzoni . Một sự khác biệt là đối tượng được tạo bởi gsl::finally()thiếu disable()cuộc gọi. Nếu bạn cần chức năng đó (giả sử, để trả lại tài nguyên sau khi nó được lắp ráp và không có ngoại lệ nào bị ràng buộc xảy ra), bạn có thể thích triển khai của Paolo. Mặt khác, sử dụng GSL gần giống với việc sử dụng các tính năng được tiêu chuẩn hóa như bạn sẽ nhận được.


3

Không thực sự, nhưng bạn có thể mô phỏng chúng đến một số phần mở rộng, ví dụ:

int * array = new int[10000000];
try {
  // Some code that can throw exceptions
  // ...
  throw std::exception();
  // ...
} catch (...) {
  // The finally-block (if an exception is thrown)
  delete[] array;
  // re-throw the exception.
  throw; 
}
// The finally-block (if no exception was thrown)
delete[] array;

Lưu ý rằng khối cuối cùng có thể tự ném một ngoại lệ trước khi ngoại lệ ban đầu được ném lại, do đó loại bỏ ngoại lệ ban đầu. Đây là hành vi chính xác giống như trong khối cuối cùng của Java. Ngoài ra, bạn không thể sử dụng returnbên trong các khối thử & bắt.


3
Tôi rất vui vì bạn đã đề cập đến khối cuối cùng có thể ném; đó là điều mà hầu hết các câu trả lời "sử dụng RAII" dường như bỏ qua. Để tránh phải viết khối cuối cùng hai lần, bạn có thể làm một cái gì đó nhưstd::exception_ptr e; try { /*try block*/ } catch (...) { e = std::current_exception(); } /*finally block*/ if (e) std::rethrow_exception(e);
sethobrien

1
Đó là tất cả những gì tôi muốn biết! Tại sao không có câu trả lời nào khác giải thích rằng một cú bắt (...) + cú ném trống; hoạt động gần giống như một khối cuối cùng? Đôi khi bạn chỉ cần nó.
VinGarcia

Giải pháp tôi cung cấp trong câu trả lời của mình ( stackoverflow.com/a/38701485/566849 ) sẽ cho phép ném ngoại lệ từ bên trong finallykhối.
Fabio A.

3

Tôi đã đưa ra một finallymacro có thể được sử dụng gần như ¹ finallytừ khóa trong Java; nó sử dụng std::exception_ptrvà bạn bè, các hàm lambda và std::promise, do đó, nó đòi hỏi C++11hoặc cao hơn; nó cũng sử dụng biểu thức câu lệnh ghép phần mở rộng GCC , cũng được hỗ trợ bởi clang.

CẢNH BÁO : phiên bản trước của câu trả lời này đã sử dụng cách triển khai khái niệm khác với nhiều hạn chế hơn.

Đầu tiên, hãy định nghĩa một lớp người trợ giúp.

#include <future>

template <typename Fun>
class FinallyHelper {
    template <typename T> struct TypeWrapper {};
    using Return = typename std::result_of<Fun()>::type;

public:    
    FinallyHelper(Fun body) {
        try {
            execute(TypeWrapper<Return>(), body);
        }
        catch(...) {
            m_promise.set_exception(std::current_exception());
        }
    }

    Return get() {
        return m_promise.get_future().get();
    }

private:
    template <typename T>
    void execute(T, Fun body) {
        m_promise.set_value(body());
    }

    void execute(TypeWrapper<void>, Fun body) {
        body();
    }

    std::promise<Return> m_promise;
};

template <typename Fun>
FinallyHelper<Fun> make_finally_helper(Fun body) {
    return FinallyHelper<Fun>(body);
}

Sau đó, có vĩ mô thực tế.

#define try_with_finally for(auto __finally_helper = make_finally_helper([&] { try 
#define finally });                         \
        true;                               \
        ({return __finally_helper.get();})) \
/***/

Nó có thể được sử dụng như thế này:

void test() {
    try_with_finally {
        raise_exception();
    }    

    catch(const my_exception1&) {
        /*...*/
    }

    catch(const my_exception2&) {
        /*...*/
    }

    finally {
        clean_it_all_up();
    }    
}

Việc sử dụng std::promiselàm cho nó rất dễ thực hiện, nhưng có lẽ nó cũng giới thiệu khá nhiều chi phí không cần thiết có thể tránh được bằng cách chỉ thực hiện lại các chức năng cần thiết từ đó std::promise.


¹ caveat: có một vài điều mà không có tác dụng khá giống như phiên bản java của finally. Off đỉnh đầu của tôi:

  1. nó không thể phá vỡ từ một vòng ngoài với breaktuyên bố từ bên trong trycatch()'s khối, vì họ sống trong một hàm lambda;
  2. phải có ít nhất một catch()khối sau try: đó là yêu cầu C ++;
  3. nếu hàm có giá trị trả về khác với void nhưng không có trả về trong các khối trycatch()'skhối, quá trình biên dịch sẽ thất bại vì finallymacro sẽ mở rộng thành mã muốn trả về a void. Điều này có thể, err, một void ed bằng cách có một finally_noreturnmacro các loại.

Nói chung, tôi không biết liệu tôi có từng sử dụng thứ này cho mình không, nhưng thật vui khi chơi với nó. :)


Vâng, đó chỉ là một bản hack nhanh, nhưng nếu lập trình viên biết những gì họ đang làm thì dù sao cũng có thể hữu ích.
Fabio A.

@MarkLakata, tôi đã cập nhật bài đăng với cách triển khai tốt hơn hỗ trợ ném ngoại lệ và trả về.
Fabio A.

Có vẻ tốt. Bạn có thể thoát khỏi Caveat 2 bằng cách đặt một catch(xxx) {}khối không thể vào lúc bắt đầu finallymacro, trong đó xxx là loại không có thật chỉ với mục đích có ít nhất một khối bắt.
Đánh dấu Lakata

@MarkLakata, tôi cũng nghĩ vậy, nhưng điều đó sẽ khiến nó không thể sử dụng được catch(...), phải không?
Fabio A.

Tôi không nghĩ vậy. Chỉ cần tạo một kiểu tối nghĩa xxxtrong một không gian tên riêng sẽ không bao giờ được sử dụng.
Đánh dấu Lakata

2

Tôi có một trường hợp sử dụng mà tôi nghĩ finally nên là một phần hoàn toàn chấp nhận được của ngôn ngữ C ++ 11, vì tôi nghĩ rằng nó dễ đọc hơn từ quan điểm lưu chuyển. Trường hợp sử dụng của tôi là một chuỗi các chuỗi tiêu dùng / nhà sản xuất, trong đó một sentinel nullptrđược gửi vào cuối quá trình để tắt tất cả các luồng.

Nếu C ++ hỗ trợ nó, bạn sẽ muốn mã của mình trông như thế này:

    extern Queue downstream, upstream;

    int Example()
    {
        try
        {
           while(!ExitRequested())
           {
             X* x = upstream.pop();
             if (!x) break;
             x->doSomething();
             downstream.push(x);
           } 
        }
        finally { 
            downstream.push(nullptr);
        }
    }

Tôi nghĩ điều này hợp lý hơn khi đưa tuyên bố cuối cùng của bạn vào đầu vòng lặp, vì nó xảy ra sau khi vòng lặp đã thoát ... nhưng đó là suy nghĩ mong muốn vì chúng ta không thể thực hiện nó trong C ++. Lưu ý rằng hàng đợi downstreamđược kết nối với một luồng khác, vì vậy bạn không thể đặt sentinel push(nullptr)vào hàm hủy downstreamvì nó không thể bị hủy tại thời điểm này ... nó cần tồn tại cho đến khi luồng khác nhận được nullptr.

Vì vậy, đây là cách sử dụng lớp RAII với lambda để làm tương tự:

    class Finally
    {
    public:

        Finally(std::function<void(void)> callback) : callback_(callback)
        {
        }
        ~Finally()
        {
            callback_();
        }
        std::function<void(void)> callback_;
    };

và đây là cách bạn sử dụng nó:

    extern Queue downstream, upstream;

    int Example()
    {
        Finally atEnd([](){ 
           downstream.push(nullptr);
        });
        while(!ExitRequested())
        {
           X* x = upstream.pop();
           if (!x) break;
           x->doSomething();
           downstream.push(x);
        }
    }

Xin chào, tôi tin rằng câu trả lời của tôi ở trên ( stackoverflow.com/a/38701485/566849 ) đáp ứng đầy đủ yêu cầu của bạn.
Fabio A.

1

Như nhiều người đã tuyên bố, giải pháp là sử dụng các tính năng của C ++ 11 để tránh các khối cuối cùng. Một trong những tính năng là unique_ptr.

Đây là câu trả lời của Mephane được viết bằng các mẫu RAII.

#include <vector>
#include <memory>
#include <list>
using namespace std;

class Foo
{
 ...
};

void DoStuff(vector<string> input)
{
    list<unique_ptr<Foo> > myList;

    for (int i = 0; i < input.size(); ++i)
    {
      myList.push_back(unique_ptr<Foo>(new Foo(input[i])));
    }

    DoSomeStuff(myList);
}

Một số giới thiệu thêm về cách sử dụng unique_ptr với các thùng chứa Thư viện chuẩn C ++ có tại đây


0

Tôi muốn cung cấp một sự thay thế.

Nếu cuối cùng bạn muốn khối được gọi luôn, chỉ cần đặt nó sau khối bắt cuối cùng (có lẽ nên catch( ... )bắt ngoại lệ không biết)

try{
   // something that might throw exception
} catch( ... ){
   // what to do with uknown exception
}

//final code to be called always,
//don't forget that it might throw some exception too
doSomeCleanUp(); 

Nếu cuối cùng bạn muốn chặn như là điều cuối cùng phải làm khi có bất kỳ ngoại lệ nào được đưa ra, bạn có thể sử dụng biến cục bộ boolean - trước khi chạy, bạn đặt nó thành false và đặt phép gán đúng ở cuối khối thử, sau đó sau khi bắt khối kiểm tra biến đó giá trị:

bool generalAppState = false;
try{
   // something that might throw exception

   //the very end of try block:
   generalAppState = true;
} catch( ... ){
   // what to do with uknown exception
}

//final code to be called only when exception was thrown,
//don't forget that it might throw some exception too
if( !generalAppState ){
   doSomeCleanUpOfDirtyEnd();
}

//final code to be called only when no exception is thrown
//don't forget that it might throw some exception too
else{
   cleanEnd();
}

Điều này không hoạt động, bởi vì toàn bộ điểm của khối cuối cùng là thực hiện dọn dẹp ngay cả khi mã phải cho phép một ngoại lệ rời khỏi khối mã. Hãy xem xét: `thử {// thứ có thể ném" B "} bắt (A & a) {} cuối cùng {// nếu C ++ có nó ... // thứ phải xảy ra, ngay cả khi" B "bị ném. } // sẽ không thực thi nếu "B" bị ném. `IMHO, điểm trừ của các trường hợp ngoại lệ là giảm mã xử lý lỗi, do đó, các khối bắt, bất cứ nơi nào có thể xảy ra, là phản tác dụng. Đây là lý do tại sao RAII giúp: nếu được áp dụng một cách tự do, ngoại lệ quan trọng nhất ở các lớp trên cùng và dưới cùng.
vất vả

1
@burlyearly ý kiến ​​của bạn không phải là thánh, tôi hiểu ý, nhưng trong C ++ thì không có gì nên bạn phải coi đây là lớp trên cùng mô phỏng hành vi này.
jave.web

TẢI XUỐNG = XIN COMMENT :)
jave.web

0

Tôi cũng nghĩ rằng RIIA không phải là sự thay thế hoàn toàn hữu ích cho việc xử lý ngoại lệ và cuối cùng cũng có. BTW, tôi cũng nghĩ RIIA là một tên xấu xung quanh. Tôi gọi các loại lớp này là 'người gác cổng' và sử dụng chúng rất nhiều. 95% thời gian họ không khởi tạo cũng không thu được tài nguyên, họ đang áp dụng một số thay đổi trên cơ sở phạm vi hoặc lấy một cái gì đó đã được thiết lập và đảm bảo rằng nó bị phá hủy. Đây là tên mẫu bị ám ảnh chính thức trên internet mà tôi bị lạm dụng thậm chí cho rằng tên của tôi có thể tốt hơn.

Tôi chỉ không nghĩ là hợp lý khi yêu cầu rằng mọi thiết lập phức tạp của một số danh sách đặc biệt phải có một lớp được viết để chứa nó để tránh các biến chứng khi làm sạch tất cả khi cần phải bắt nhiều loại ngoại lệ nếu có lỗi xảy ra trong quy trình. Điều này sẽ dẫn đến rất nhiều lớp học ad hoc mà không cần thiết nếu không.

Có, nó tốt cho các lớp được thiết kế để quản lý một tài nguyên cụ thể hoặc các lớp chung được thiết kế để xử lý một tập hợp các tài nguyên tương tự. Nhưng, ngay cả khi tất cả những thứ liên quan đều có các trình bao bọc như vậy, việc phối hợp dọn dẹp có thể không chỉ đơn giản trong việc gọi ngược lại các lệnh hủy diệt.

Tôi nghĩ rằng nó có ý nghĩa hoàn hảo cho C ++ để có một cuối cùng. Ý tôi là, trời ạ, rất nhiều bit và bob đã được dán vào nó trong nhiều thập kỷ qua đến nỗi có vẻ như những người kỳ quặc sẽ đột nhiên trở nên bảo thủ đối với một cái gì đó giống như cuối cùng có thể khá hữu ích và có lẽ không có gì phức tạp như một số thứ khác đã xảy ra đã thêm (mặc dù đó chỉ là phỏng đoán của tôi.)


-2
try
{
  ...
  goto finally;
}
catch(...)
{
  ...
  goto finally;
}
finally:
{
  ...
}

35
Thành ngữ dễ thương, nhưng nó không hoàn toàn giống nhau. trở lại trong khối thử hoặc bắt sẽ không chuyển qua mã 'cuối cùng:' của bạn.
Edward KMett

10
Thật đáng để giữ câu trả lời sai này (với 0 đánh giá), vì Edward Kmett mang đến một sự khác biệt rất quan trọng.
Đánh dấu Lakata

12
Lỗ hổng thậm chí còn lớn hơn (IMO): Mã này ăn tất cả các ngoại lệ, điều finallynày không làm.
Ben Voigt
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.