2009-05-01 38 views
222

Tôi đang có cuộc tranh luận với đồng nghiệp về việc ném ngoại lệ từ các nhà thầu và nghĩ rằng tôi muốn một số phản hồi.Ném ngoại lệ từ các nhà thầu

Có ok để ném ngoại lệ từ các nhà thầu, từ quan điểm thiết kế không?

phép nói rằng tôi đang quấn một mutex posix trong một lớp học, nó sẽ giống như thế này:

class Mutex { 
public: 
    Mutex() { 
    if (pthread_mutex_init(&mutex_, 0) != 0) { 
     throw MutexInitException(); 
    } 
    } 

    ~Mutex() { 
    pthread_mutex_destroy(&mutex_); 
    } 

    void lock() { 
    if (pthread_mutex_lock(&mutex_) != 0) { 
     throw MutexLockException(); 
    } 
    } 

    void unlock() { 
    if (pthread_mutex_unlock(&mutex_) != 0) { 
     throw MutexUnlockException(); 
    } 
    } 

private: 
    pthread_mutex_t mutex_; 
}; 

Câu hỏi của tôi là, đây là cách tiêu chuẩn để làm điều đó? Bởi vì nếu cuộc gọi mutex_init pthread thất bại thì đối tượng mutex là không thể sử dụng được vì vậy việc ném một ngoại lệ đảm bảo rằng mutex sẽ không được tạo ra.

Tôi có nên tạo một hàm thành viên init cho lớp Mutex và gọi là mutex_init pthread trong đó sẽ trả về một bool dựa trên sự trở lại của mutex_init của pthread không? Bằng cách này tôi không phải sử dụng ngoại lệ cho một đối tượng cấp thấp như vậy.

+0

Một liên kết khác có liên quan đến chủ đề: http://www.writeulearn.com/exception-constructor/ –

+0

Vâng, bạn có thể bỏ qua các ctors nhiều như nó là từ bất kỳ chức năng nào khác, điều đó được cho là bạn nên cẩn thận từ bất kỳ chức năng nào. – g24l

+3

Một cái gì đó không liên quan: tại sao không loại bỏ khóa/mở khóa phương pháp của bạn, và trực tiếp khóa mutex trong constructor và mở khóa trong destructor?Bằng cách đó, chỉ cần khai báo một biến tự động trong một phạm vi tự động khóa/mở khóa, không cần phải quan tâm đến các ngoại lệ, trả về, v.v ... Xem 'std :: lock_guard' để thực hiện tương tự. –

Trả lời

207

Có, việc ném ngoại lệ từ hàm tạo không thành công là cách tiêu chuẩn để thực hiện việc này. Hãy đọc Câu hỏi thường gặp này về Handling a constructor that fails để biết thêm thông tin. Có một phương thức init() cũng sẽ làm việc, nhưng tất cả mọi người tạo ra đối tượng của mutex phải nhớ rằng init() phải được gọi. Tôi cảm thấy nó đi ngược lại nguyên tắc RAII.

+13

Trong hầu hết các trường hợp. Đừng quên những thứ như std :: fstream. Khi thất bại nó vẫn tạo ra một đối tượng, nhưng bởi vì chúng tôi luôn kiểm tra trạng thái của đối tượng thông thường nó hoạt động tốt. Vì vậy, một đối tượng có trạng thái tự nhiên được kiểm tra theo cách sử dụng bình thường có thể không cần phải ném. –

+1

@Widor: Cảm ơn bạn đã đánh giá bản chỉnh sửa được đề xuất của tôi. 278978. Tôi hỏi thêm một câu hỏi liên quan đến chỉnh sửa? Câu trả lời mà nhận xét này được đính kèm có liên kết lỗi thời. Để khắc phục, nó muốn thay đổi chính xác một ký tự, thay thế "# faq-17.2" bằng "# faq-17.8" trong URL. Tuy nhiên, phần mềm của Stackoverflow yêu cầu chỉnh sửa do một người dùng danh tiếng thấp gửi đến như tôi thay đổi ít nhất sáu ký tự. Khá rõ ràng, liên kết bị hỏng muốn được sửa và nó không phải là bản sửa lỗi sáu ký tự. Bạn có biết làm thế nào tôi có thể sửa chữa nó, xin vui lòng? – thb

+4

Không thực sự, trong trường hợp cụ thể này, lưu ý rằng mutex destructor của mình sẽ không bao giờ được gọi, có thể rò rỉ các mutex pthread. Các giải pháp cho điều đó là sử dụng một con trỏ thông minh cho các mutex pthread, tốt hơn nhưng sử dụng mutexes tăng hoặc std :: mutex, không có lý do để tiếp tục sử dụng cấu trúc hệ điều hành kiểu chức năng cũ khi có lựa chọn thay thế tốt hơn. –

0

Mặc dù tôi đã không làm việc C++ ở cấp độ chuyên nghiệp, theo ý kiến ​​của tôi, nó là OK để ném ngoại lệ từ các nhà thầu. Tôi làm điều đó (nếu cần) trong. Net. Kiểm tra liên kết thisthis. Nó có thể là sự quan tâm của bạn.

+10

.NET không phải là C++, không phải JAVA. Cơ chế ném không giống nhau và chi phí khác nhau. – g24l

30

Ném ngoại lệ là cách tốt nhất để đối phó với lỗi của hàm tạo. Bạn nên đặc biệt tránh nửa xây dựng một đối tượng và sau đó dựa vào người dùng của lớp của bạn để phát hiện lỗi xây dựng bằng cách kiểm tra các biến cờ của một số loại.

Trên một điểm có liên quan, thực tế là bạn có một số loại ngoại lệ khác nhau để xử lý các lỗi mutex khiến tôi hơi lo lắng. Thừa kế là một công cụ tuyệt vời, nhưng nó có thể được sử dụng quá mức. Trong trường hợp này, tôi có lẽ sẽ thích một ngoại lệ MutexError duy nhất, có thể chứa một thông báo lỗi thông tin.

+0

Tôi muốn điểm thứ hai của Neil về chế độ thừa kế ngoại lệ - một MutexError đơn lẻ có thể là lựa chọn tốt hơn trừ khi bạn đặc biệt muốn xử lý lỗi khóa khác nhau. Nếu bạn có quá nhiều loại ngoại lệ, việc bắt tất cả chúng có thể trở nên mệt mỏi và dễ bị lỗi. – markh44

+0

Tôi đồng ý rằng một loại ngoại lệ mutex là đủ. Và điều này cũng sẽ làm cho việc xử lý lỗi trực quan hơn. – lkristjansen

85

Nếu bạn ném một ngoại lệ từ một hàm tạo, hãy nhớ rằng bạn cần sử dụng cú pháp try/catch của hàm nếu bạn cần nắm bắt ngoại lệ đó trong danh sách khởi tạo hàm tạo.

ví dụ:

func::func() : foo() 
{ 
    try {...} 
    catch (...) // will NOT catch exceptions thrown from foo constructor 
    { ... } 
} 

vs

func::func() 
    try : foo() {...} 
    catch (...) // will catch exceptions thrown from foo constructor 
    { ... } 
+25

Cần lưu ý rằng các trường hợp ngoại lệ được nêu ra từ việc xây dựng một đối tượng phụ không thể bị chặn: http://www.gotw.ca/gotw/066.htm –

10

It is OK để ném từ constructor của bạn, nhưng bạn nên chắc chắn rằng đối tượng của bạn được xây dựng sau khi chính đã bắt đầu và trước khi nó kết thúc :

class A 
{ 
public: 
    A() { 
    throw int(); 
    } 
}; 

A a;  // Implementation defined behaviour if exception is thrown (15.3/13) 

int main() 
{ 
    try 
    { 
    // Exception for 'a' not caught here. 
    } 
    catch (int) 
    { 
    } 
} 
2

Thời gian duy nhất bạn KHÔNG ném ngoại lệ từ nhà thầu là nếu dự án của bạn có quy tắc chống lại việc sử dụng ngoại lệ (ví dụ: Google không thích ngoại lệ). Trong trường hợp đó, bạn sẽ không muốn sử dụng các ngoại lệ trong constructor của bạn ở bất kỳ nơi nào khác, và thay vào đó bạn phải có một phương thức init.

+0

Bạn có thể quan tâm đến cuộc thảo luận dài về các nguyên tắc của Google tại http://groups.google.com/group/comp.lang.c++.moderated/browse_thread/thread/d7b0a5c663467471/2e5707c44726925a?lnk=gst&q=google#2e5707c44726925a –

+4

Thảo luận thú vị. Ý kiến ​​cá nhân của tôi là bạn chỉ nên sử dụng ngoại lệ khi bạn thực sự thiết kế cấu trúc xử lý lỗi của chương trình để tận dụng chúng. Nếu bạn cố gắng xử lý lỗi sau khi viết mã, hoặc cố gắng shoehorn ngoại lệ vào các chương trình không được viết cho chúng, nó sẽ dẫn đến hoặc thử/bắt MỌI NGƯỜI (loại bỏ các ưu điểm của ngoại lệ) hoặc các chương trình bị lỗi tại ít lỗi nhất. Tôi đối phó với cả hai ngày và tôi không thích nó. –

3

Nếu dự án của bạn thường dựa vào ngoại lệ để phân biệt dữ liệu xấu khỏi dữ liệu tốt, thì việc ném ngoại lệ từ hàm tạo là giải pháp tốt hơn là không ném. Nếu ngoại lệ không được ném, thì đối tượng được khởi tạo trong trạng thái zombie. Đối tượng như vậy cần để lộ một lá cờ cho biết đối tượng có đúng hay không. Một cái gì đó như thế này:

class Scaler 
{ 
    public: 
     Scaler(double factor) 
     { 
      if (factor == 0) 
      { 
       _state = 0; 
      } 
      else 
      { 
       _state = 1; 
       _factor = factor; 
      } 
     } 

     double ScaleMe(double value) 
     { 
      if (!_state) 
       throw "Invalid object state."; 
      return value/_factor; 
     } 

     int IsValid() 
     { 
      return _status; 
     } 

    private: 
     double _factor; 
     int _state; 

} 

Sự cố với phương pháp này nằm ở phía người gọi. Mỗi người dùng của lớp sẽ phải làm một nếu trước khi thực sự sử dụng đối tượng. Đây là một cuộc gọi cho các lỗi - không có gì đơn giản hơn là quên để kiểm tra một điều kiện trước khi tiếp tục.

Trong trường hợp ném ngoại lệ từ hàm tạo, thực thể tạo đối tượng được cho là sẽ xử lý sự cố ngay lập tức. Đối tượng người tiêu dùng xuống dòng là miễn phí để giả định rằng đối tượng là 100% hoạt động từ thực tế chỉ là họ có được nó.

Cuộc thảo luận này có thể tiếp tục theo nhiều hướng.

Ví dụ: sử dụng ngoại lệ như một vấn đề xác thực là một thực tế không tốt. Một cách để làm điều đó là một mẫu Thử kết hợp với lớp nhà máy. Nếu bạn đã sử dụng các nhà máy, sau đó viết hai phương pháp:

class ScalerFactory 
{ 
    public: 
     Scaler CreateScaler(double factor) { ... } 
     int TryCreateScaler(double factor, Scaler **scaler) { ... }; 
} 

Với giải pháp này, bạn có thể lấy lá cờ tình trạng tại chỗ, như một giá trị trả về của phương pháp nhà máy, mà không bao giờ bước vào constructor với dữ liệu xấu .

Điều thứ hai là nếu bạn bao gồm mã bằng các thử nghiệm tự động. Trong trường hợp đó, mỗi đoạn mã sử dụng đối tượng không ném ngoại lệ sẽ phải được bao phủ bằng một phép thử bổ sung - cho dù nó hoạt động đúng khi phương thức IsValid() trả về false. Điều này giải thích khá tốt rằng việc khởi tạo các đối tượng trong trạng thái zombie là một ý tưởng tồi.

3

Ngoài thực tế mà bạn không cần phải ném từ các nhà xây dựng trong trường hợp cụ thể của bạn vì pthread_mutex_lock actually returns an EINVAL if your mutex has not been initialized và bạn có thể ném sau khi cuộc gọi đến lock như được thực hiện trong std::mutex:

void 
lock() 
{ 
    int __e = __gthread_mutex_lock(&_M_mutex); 

    // EINVAL, EAGAIN, EBUSY, EINVAL, EDEADLK(may) 
    if (__e) 
__throw_system_error(__e); 
} 

sau đó trong nói chung ném từ các nhà thầu là ok cho mua lại lỗi trong khi xây dựng và tuân theo mô hình lập trình RAII (Nguồn-chuyển đổi-đang-khởi tạo).

Kiểm tra này example on RAII

void write_to_file (const std::string & message) { 
    // mutex to protect file access (shared across threads) 
    static std::mutex mutex; 

    // lock mutex before accessing file 
    std::lock_guard<std::mutex> lock(mutex); 

    // try to open file 
    std::ofstream file("example.txt"); 
    if (!file.is_open()) 
     throw std::runtime_error("unable to open file"); 

    // write message to file 
    file << message << std::endl; 

    // file will be closed 1st when leaving scope (regardless of exception) 
    // mutex will be unlocked 2nd (from lock destructor) when leaving 
    // scope (regardless of exception) 
} 

Tập trung vào các báo cáo:

  1. static std::mutex mutex
  2. std::lock_guard<std::mutex> lock(mutex);
  3. std::ofstream file("example.txt");

Câu lệnh đầu tiên là RAII và noexcept.Trong (2) rõ ràng rằng RAII được áp dụng trên lock_guard và nó thực sự có thể throw, trong khi ở (3) ofstream có vẻ không phải là RAII, vì trạng thái đối tượng phải được kiểm tra bằng cách gọi is_open() để kiểm tra cờ failbit.

Thoạt nhìn có vẻ như nó là chưa quyết định về những gì nó chuẩn cách và trong trường hợp đầu tiên std::mutex không ném trong phần khởi tạo * trái ngược với thực hiện OP *. Trong trường hợp thứ hai nó sẽ ném bất cứ điều gì được ném từ std::mutex::lock, và trong thứ ba không có ném cả.

Chú ý sự khác biệt:

(1) có thể được khai báo tĩnh, và thực sự sẽ được công bố như là một biến thành viên (2) Sẽ không bao giờ thực sự được dự kiến ​​sẽ được công bố như là một biến thành viên (3) dự kiến ​​sẽ được khai báo dưới dạng biến thành viên và tài nguyên cơ bản có thể không luôn có sẵn.

Tất cả các biểu mẫu này là RAII; để giải quyết vấn đề này, người ta phải phân tích RAII.

  • Resource: đối tượng của bạn
  • Acquisition (phân bổ): bạn đối tượng được tạo ra
  • Khởi tạo: đối tượng của bạn ở trạng thái bất biến của nó

này không yêu cầu bạn phải khởi tạo và kết nối mọi thứ trên công trình. Ví dụ khi bạn sẽ tạo một đối tượng khách hàng mạng, bạn sẽ không thực sự kết nối nó với máy chủ khi tạo, vì nó là một hoạt động chậm với các lỗi. Thay vào đó, bạn sẽ viết một hàm connect để thực hiện điều đó. Mặt khác, bạn có thể tạo bộ đệm hoặc chỉ đặt trạng thái của bộ đệm.

Do đó, vấn đề của bạn sẽ giảm xuống để xác định trạng thái ban đầu của bạn. Nếu trong trường hợp của bạn, trạng thái ban đầu của bạn là mutex phải được khởi tạo thì bạn nên ném từ hàm tạo. Ngược lại nó chỉ là tốt để không khởi tạo sau đó (như được thực hiện trong std::mutex), và xác định trạng thái bất biến của bạn là mutex được tạo ra. Ở bất kỳ mức nào, bất biến không nhất thiết phải bị xâm phạm bởi trạng thái đối tượng thành viên của nó, kể từ khi đối tượng mutex_ biến đổi giữa lockedunlocked thông qua phương thức công khai Mutex::lock()Mutex::unlock().

class Mutex { 
private: 
    int e; 
    pthread_mutex_t mutex_; 

public: 
    Mutex(): e(0) { 
    e = pthread_mutex_init(&mutex_); 
    } 

    void lock() { 

    e = pthread_mutex_lock(&mutex_); 
    if(e == EINVAL) 
    { 
     throw MutexInitException(); 
    } 
    else (e) { 
     throw MutexLockException(); 
    } 
    } 

    // ... the rest of your class 
}; 
8
#include <iostream> 

class bar 
{ 
public: 
    bar() 
    { 
    std::cout << "bar() called" << std::endl; 
    } 

    ~bar() 
    { 
    std::cout << "~bar() called" << std::endl; 

    } 
}; 
class foo 
{ 
public: 
    foo() 
    : b(new bar()) 
    { 
    std::cout << "foo() called" << std::endl; 
    throw "throw something"; 
    } 

    ~foo() 
    { 
    delete b; 
    std::cout << "~foo() called" << std::endl; 
    } 

private: 
    bar *b; 
}; 


int main(void) 
{ 
    try { 
    std::cout << "heap: new foo" << std::endl; 
    foo *f = new foo(); 
    } catch (const char *e) { 
    std::cout << "heap exception: " << e << std::endl; 
    } 

    try { 
    std::cout << "stack: foo" << std::endl; 
    foo f; 
    } catch (const char *e) { 
    std::cout << "stack exception: " << e << std::endl; 
    } 

    return 0; 
} 

đầu ra:

heap: new foo 
bar() called 
foo() called 
heap exception: throw something 
stack: foo 
bar() called 
foo() called 
stack exception: throw something 

destructors không được gọi, vì vậy nếu một ngoại lệ cần phải được ném vào một constructor, rất nhiều thứ (ví dụ như dọn dẹp?) Để làm .

+0

Điểm rất tốt. Tôi ngạc nhiên rằng không có câu trả lời nào khác giải quyết loại rò rỉ này. – Carlton

+3

Bạn nên sử dụng tiêu chuẩn :: unique_ptr hoặc tương tự. Destructor của các thành viên được gọi là nếu một ngoại lệ được ném trong khi xây dựng, nhưng con trỏ đồng bằng không có bất kỳ. Thay thế 'bar * b' bằng' std :: unique_ptr b' (bạn sẽ phải xóa 'delete b;' và thêm tiêu đề '') và chạy lại. – cbuchart

+1

Hành vi này khá hợp lý. Nếu các nhà xây dựng đã thất bại (không hoàn thành thành công) tại sao các destructor được gọi là? Nó không có gì để làm sạch và nếu đã cố gắng để làm sạch các đối tượng mà thậm chí không được instantiated đúng (nghĩ rằng một số con trỏ), nó sẽ gây ra nhiều vấn đề hơn, không cần thiết. – zar

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