2012-07-05 24 views
29

Tôi quen thuộc với những ưu điểm của RAII, nhưng gần đây tôi vấp một vấn đề trong mã như thế này:Làm thế nào để xử lý thất bại constructor cho RAII

class Foo 
{ 
    public: 
    Foo() 
    { 
    DoSomething(); 
    ...  
    } 

    ~Foo() 
    { 
    UndoSomething(); 
    } 
} 

Tất cả tốt, ngoại trừ mã trong phần constructor ... đã ném một ngoại lệ, với kết quả là UndoSomething() không bao giờ được gọi.

Có những cách rõ ràng để khắc phục sự cố cụ thể đó, như quấn ... trong khối try/catch rồi gọi UndoSomething(), nhưng: đó là mã trùng lặp và b: khối try/catch là một mã mà tôi thử tránh bằng cách sử dụng kỹ thuật RAII. Và, mã có khả năng trở nên tồi tệ hơn và dễ bị lỗi hơn nếu có nhiều cặp Do/Undo liên quan, và chúng ta phải dọn dẹp nửa chừng.

Tôi tự hỏi có cách tiếp cận tốt hơn để thực hiện điều này - có thể một đối tượng riêng biệt sẽ lấy một con trỏ hàm và gọi hàm khi nó bị hủy?

class Bar 
{ 
    FuncPtr f; 
    Bar() : f(NULL) 
    { 
    } 

    ~Bar() 
    { 
    if (f != NULL) 
     f(); 
    } 
} 

Tôi biết rằng sẽ không biên dịch nhưng nó sẽ hiển thị nguyên tắc. Foo sau đó trở thành ...

class Foo 
{ 
    Bar b; 

    Foo() 
    { 
    DoSomething(); 
    b.f = UndoSomething; 
    ...  
    } 
} 

Lưu ý rằng foo bây giờ không yêu cầu hủy. Điều đó nghe có vẻ rắc rối hơn nó đáng giá, hay đây là một mô hình chung với thứ gì đó hữu ích trong việc tăng cường xử lý việc nâng hạng nặng cho tôi?

+3

try/catch là _not_ code smell và thường không được sử dụng IMO. –

+2

xem tại đây: http://www.parashift.com/c++-faq-lite/selfcleaning-members.html – MadScientist

+2

@MooingDuck: Thật vậy, bản thân chúng không có mùi. Nhưng 'try {} catch (...) {throw;} 'có mùi khá mạnh. –

Trả lời

30

Vấn đề là lớp học của bạn đang cố gắng làm quá nhiều. Nguyên tắc của RAII là nó mua lại một tài nguyên (hoặc trong constructor, hoặc sau này), và destructor giải phóng nó; lớp tồn tại chỉ để quản lý tài nguyên đó.

Trong trường hợp của bạn, bất kỳ điều gì khác ngoài DoSomething()UndoSomething() phải thuộc trách nhiệm của người dùng của lớp học, chứ không phải là chính lớp đó.

Như Steve Jessop đã nói trong nhận xét: nếu bạn có nhiều tài nguyên để có được, thì mỗi tài khoản phải được quản lý bởi đối tượng RAII của chính nó; và có thể hợp lý để tổng hợp chúng thành các thành viên dữ liệu của một lớp khác mà lần lượt xây dựng từng lớp. Sau đó, nếu bất kỳ việc mua lại nào bị lỗi, tất cả các tài nguyên được mua trước đây sẽ được tự động phát hành bởi các destructors của các thành viên trong lớp.

(Ngoài ra, hãy nhớ Rule of Three; lớp học của bạn cần phải ngăn chặn sao chép hoặc triển khai theo cách hợp lý để ngăn nhiều cuộc gọi đến UndoSomething()).

+5

Điều tôi định nói. Tôi sẽ thêm rằng khi bạn đã viết một lớp để quản lý từng tài nguyên, bạn có thể tổng hợp một số trong số chúng lại với nhau thành các thành viên dữ liệu của một lớp khác, nếu sự kết hợp các tài nguyên có ý nghĩa. Nếu một nhà xây dựng thành viên dữ liệu ném, thì bất kỳ thành viên nào đã khởi tạo đều bị hủy. –

+0

Có, nhưng Sự chiếm hữu của tài nguyên liên quan đến một số bước: 'DoSomething' là bước đầu tiên trong việc mua tài nguyên. – Roddy

+5

@Roddy: ITYM, "có một số tài nguyên để có được". Bạn có thể chưa nhận ra rằng chúng là các tài nguyên riêng biệt, nhưng mẫu RAII đang cố gắng hết sức để nói cho bạn biết :-) –

6

tôi sẽ giải quyết việc này bằng RAII, quá:

class Doer 
{ 
    Doer() 
    { DoSomething(); } 
    ~Doer() 
    { UndoSomething(); } 
}; 
class Foo 
{ 
    Doer doer; 
public: 
    Foo() 
    { 
    ... 
    } 
}; 

Các người làm được tạo ra trước khi bắt đầu ctor cơ thể và bị phá hủy hoặc khi destructor không qua một ngoại lệ hoặc khi đối tượng bị phá hủy bình thường.

17

Chỉ cần chắc DoSomething/UndoSomething thành một tay cầm RAII thích hợp:

struct SomethingHandle 
{ 
    SomethingHandle() 
    { 
    DoSomething(); 
    // nothing else. Now the constructor is exception safe 
    } 

    SomethingHandle(SomethingHandle const&) = delete; // rule of three 

    ~SomethingHandle() 
    { 
    UndoSomething(); 
    } 
} 


class Foo 
{ 
    SomethingHandle something; 
    public: 
    Foo() : something() { // all for free 
     // rest of the code 
    } 
} 
+0

'= delete' là gì? – Nick

+2

@Nick nó làm cho trình biên dịch thất bại nếu chức năng đó được chọn bởi độ phân giải quá tải. Đó là một tính năng mới. Bạn có thể đạt được một cái gì đó tương tự trong các trình biên dịch không hỗ trợ tính năng này bằng cách làm cho nó riêng tư. –

6

Bạn đã có quá nhiều trong một lớp học của bạn.Di chuyển DoSomething/UndoSomething đến lớp khác ('Something'), và có một đối tượng của lớp đó như là một phần của lớp Foo, thusly:

class Foo 
{ 
    public: 
    Foo() 
    { 
    ...  
    } 

    ~Foo() 
    { 
    } 

    private: 
    class Something { 
    Something() { DoSomething(); } 
    ~Something() { UndoSomething(); } 
    }; 
    Something s; 
} 

Bây giờ, DoSomething đã được gọi bằng thời gian constructor Foo được gọi là, và nếu hàm tạo của Foo ném, thì UndoSomething được gọi đúng.

6

try/catch is không phải là mùi mã nói chung, nó nên được sử dụng để xử lý lỗi. Trong trường hợp của bạn mặc dù, nó sẽ là mã mùi, bởi vì nó không xử lý một lỗi, chỉ đơn thuần là làm sạch. Thats những gì destructors được cho.

(1) Nếu mọi thứ trong trình phá hủy phải được gọi khi hàm tạo không thành công, chỉ cần di chuyển đến hàm làm sạch riêng, được gọi bởi hàm hủy và hàm tạo trong trường hợp thất bại. Điều này có vẻ là những gì bạn đã làm. Làm tốt lắm.

(2) Ý tưởng tốt hơn là: Nếu có nhiều/hoàn tác các cặp có thể hủy riêng biệt, chúng phải được bao bọc trong lớp RAII nhỏ của riêng chúng, nó làm tối thiểu và tự dọn dẹp. Tôi không thích ý tưởng hiện tại của bạn cho nó một chức năng con trỏ dọn dẹp tùy chọn, đó chỉ là khó hiểu. Việc dọn dẹp nên luôn luôn được ghép nối với việc khởi tạo, đó là khái niệm cốt lõi của RAII.

+0

Cảm ơn. Đồng ý tái. ngoại lệ - nhưng tôi đã thấy rất nhiều mã dọn dẹp trong các câu lệnh bắt mà tôi vẫn đối xử với họ với sự nghi ngờ. Khái niệm là chức năng dọn dẹp là 'tùy chọn' để tránh gọi UndoSomething nếu có một ngoại lệ được ném trước khi DoSomething được gọi. 'void Foo(): b (UndoSomething) {... DoSomething());' – Roddy

+0

@Roddy: Nếu dọn dẹp nằm trong miniclass, việc khởi tạo cũng phải nằm trong miniclass, làm cho điều đó không thành vấn đề. –

+0

âm thanh như 'void Foo(): b (DoSomething, UndoSomething)' là bắt buộc. Thú vị ... – Roddy

0

Quy tắc ngón tay cái:

  • Nếu lớp học của bạn được tự quản lý việc tạo và xóa một cái gì đó, nó đang làm quá nhiều.
  • Nếu lớp học của bạn đã tự viết sao chép phân/-Xây dựng, nó có lẽ là quản lý quá nhiều
  • ngoại lệ như thế này: Một lớp học có mục đích duy nhất của việc quản lý chính xác một thực thể

ví dụ cho quy tắc thứ ba là std::shared_ptr, std::unique_ptr, scope_guard, std::vector<>, std::list<>, scoped_lock và dĩ nhiên là lớp Trasher bên dưới.


Phụ lục.

Bạn có thể đi xa và viết một cái gì đó tương tác với C-style thứ:

#include <functional> 
#include <iostream> 
#include <stdexcept> 


class Trasher { 
public: 
    Trasher (std::function<void()> init, std::function<void()> deleter) 
    : deleter_(deleter) 
    { 
     init(); 
    } 

    ~Trasher() 
    { 
     deleter_(); 
    } 

    // non-copyable 
    Trasher& operator= (Trasher const&) = delete; 
    Trasher (Trasher const&) = delete; 

private: 
    std::function<void()> deleter_; 
}; 

class Foo { 
public: 
    Foo() 
    : meh_([](){std::cout << "hello!" << std::endl;}, 
      [](){std::cout << "bye!" << std::endl;}) 
    , moo_([](){std::cout << "be or not" << std::endl;}, 
      [](){std::cout << "is the question" << std::endl;}) 
    { 
     std::cout << "Fooborn." << std::endl; 
     throw std::runtime_error("oh oh"); 
    } 

    ~Foo() { 
     std::cout << "Foo in agony." << std::endl; 
    } 

private: 
    Trasher meh_, moo_; 
}; 

int main() { 
    try { 
     Foo foo; 
    } catch(std::exception &e) { 
     std::cerr << "error:" << e.what() << std::endl; 
    } 
} 

đầu ra:

hello! 
be or not 
Fooborn. 
is the question 
bye! 
error:oh oh 

Vì vậy, ~Foo() không bao giờ chạy, nhưng init của bạn/xóa cặp là.

Một điều thú vị là: Nếu hàm init của bạn tự động ném, hàm xóa của bạn sẽ không được gọi, vì bất kỳ ngoại lệ nào được ném bởi hàm init đi thẳng qua Trasher() và do đó ~Trasher() sẽ không được thực hiện.

Lưu ý: Điều quan trọng là có một bên ngoài nhất là try/catch, nếu không, yêu cầu ngăn xếp không được yêu cầu theo tiêu chuẩn.

Các vấn đề liên quan