2014-09-02 26 views
28

Tại sao luồng Thư viện chuẩn C++ sử dụng open()/close() ngữ nghĩa được tách riêng khỏi thời gian tồn tại của đối tượng? Kết thúc hủy diệt vẫn có thể làm cho các lớp RAII kỹ thuật, nhưng việc mua lại/giải phóng độc lập để lại các lỗ hổng trong phạm vi mà tay cầm có thể trỏ tới không có gì nhưng vẫn cần kiểm tra thời gian chạy.Tại sao luồng tệp chuẩn C++ không tuân theo quy ước RAII chặt chẽ hơn?

Tại sao các nhà thiết kế thư viện chọn cách tiếp cận của họ hơn là chỉ mở cửa trong các nhà thầu mà ném vào một thất bại?

void foo() { 
    std::ofstream ofs; 
    ofs << "Can't do this!\n"; // XXX 
    ofs.open("foo.txt"); 

    // Safe access requires explicit checking after open(). 
    if (ofs) { 
    // Other calls still need checks but must be shielded by an initial one. 
    } 

    ofs.close(); 
    ofs << "Whoops!\n"; // XXX 
} 

// This approach would seem better IMO: 
void bar() { 
    std_raii::ofstream ofs("foo.txt"); // throw on failure and catch wherever 
    // do whatever, then close ofs on destruction ... 
} 

Một từ ngữ tốt hơn của câu hỏi có thể là lý do tại sao quyền truy cập không mở là fstream là điều đáng từng có. Kiểm soát thời gian mở tập tin thông qua cuộc đời xử lý dường như không phải là một gánh nặng gì cả, nhưng thực sự là một lợi ích an toàn.

+0

Có, nó chắc chắn thiếu giá trị chế độ 'throw_exception'. Có thể thiết lập các ngoại lệ cho các hoạt động sau này, nhưng việc ném constructor sẽ tốt hơn. –

Trả lời

33

Mặc dù các câu trả lời khác là hợp lệ và hữu ích, tôi cho rằng lý do thực sự thực sự là đơn giản hơn.

Thiết kế iostreams cũ hơn nhiều so với Thư viện chuẩn và trước đó sử dụng rộng rãi các ngoại lệ. Tôi nghi ngờ rằng để tương thích với mã hiện có, việc sử dụng các ngoại lệ đã được thực hiện tùy chọn, không phải là mặc định cho việc không mở được một tệp.

Ngoài ra, câu hỏi của bạn chỉ thực sự liên quan đến luồng tệp, các loại luồng chuẩn khác không có chức năng thành viên open() hoặc close(), do đó nhà thầu của họ không ném nếu không mở được tệp :-)

Đối với các file, bạn có thể muốn kiểm tra xem các cuộc gọi close() đã thành công, vì vậy bạn biết nếu các dữ liệu đã ghi vào đĩa, vì vậy đó là một lý do chính đáng không để làm điều đó trong destructor, bởi vì vào thời điểm đối tượng bị phá hủy nó đã quá muộn để làm bất cứ điều gì hữu ích với nó và bạn gần như chắc chắn không muốn ném một ngoại lệ từ destructor.Vì vậy, một fstreambuf sẽ gọi đóng trong destructor của nó, nhưng bạn cũng có thể làm điều đó bằng tay trước khi phá hủy nếu bạn muốn.

Trong mọi trường hợp, tôi không đồng ý rằng nó không theo quy ước RAII ...

Tại sao các nhà thiết kế thư viện chọn cách tiếp cận của họ đối với việc mở chỉ trong cấu trúc cho ném vào một thất bại?

N.B. RAII không có nghĩa là bạn không thể có thành viên riêng biệt open() ngoài một nhà xây dựng tài nguyên, hoặc bạn không thể dọn sạch tài nguyên trước khi hủy diệt, ví dụ: unique_ptr có thành viên reset().

Ngoài ra, RAII không có nghĩa là bạn phải ném vào thất bại hoặc một đối tượng không thể ở trạng thái trống, ví dụ: unique_ptr có thể được xây dựng với một con trỏ null hoặc mặc định xây dựng, và do đó cũng có thể trỏ đến không có gì và vì vậy trong một số trường hợp, bạn cần phải kiểm tra nó trước khi dereferencing.

Luồng tệp có được tài nguyên về xây dựng và giải phóng nó khi hủy - đó là RAII theo như tôi quan tâm. Những gì bạn đang phản đối là yêu cầu một kiểm tra, mà mùi của hai giai đoạn khởi tạo, và tôi đồng ý rằng có một chút mùi. Nó không làm cho nó không RAII mặc dù.

Trong quá khứ tôi đã giải quyết mùi với lớp CheckedFstream, đây là một trình bao bọc đơn giản bổ sung thêm một tính năng: ném vào cosntructor nếu không thể mở luồng. Trong C++ 11 đó là đơn giản như này:

struct CheckedFstream : std::fstream 
{ 
    CheckedFstream() = default; 

    CheckedFstream(std::string const& path, std::ios::openmode m = std::ios::in|std::ios::out) 
    : fstream(path, m) 
    { if (!is_open()) throw std::ios::failure("Could not open " + path); } 
}; 
+0

Bạn đúng về khía cạnh 'fstream'. Tiêu đề câu hỏi viết lại ... – Jeff

+0

thực tế thú vị về lý do ngoại lệ không phải là mặc định. – bolov

+0

Đúng, 'không' là lỗi đánh máy, hiện đã được sửa. Ngoài ra, tôi đã giả định rằng trình tự khởi tạo một giai đoạn là một phần cốt lõi của triết lý RAII, không chỉ là một phương pháp tiếp cận thiết kế bổ sung. – Jeff

7

Các nhà thiết kế thư viện cho bạn lựa chọn:

std::ifstream file{}; 
file.exceptions(std::ifstream::failbit | std::ifstream::badbit); 

try 
{ 
    file.open(path); // now it will throw on failure 
} 
catch (const std::ifstream::failure& e) 
{ 
} 
+0

Đây là một điểm rất tốt, nhưng nó vẫn để lại ngoại lệ mặt nạ tách rời từ việc tạo ra cá thể và lá mở khả năng truy cập vào các tập tin không mở mà phải được bắt tại thời gian chạy. – Jeff

+0

@ Jeff: có vẻ như tôi không nhận được câu hỏi của bạn chút nào, đóng luồng không hợp lệ mở ra không gây ra sự cố: "Nếu thao tác không thành công (bao gồm cả khi không có tệp nào được mở trước cuộc gọi), cờ trạng thái failbit được đặt cho luồng" –

+0

Xin lỗi, tôi có nghĩa là truy cập vào luồng tệp sẽ không bao giờ hoạt động trước khi mở sau khi xây dựng gần hoặc không mở nhưng trạng thái luồng (hoặc một ngoại lệ được bật) sẽ phải kích hoạt lỗi thời gian chạy thay vì sử dụng phạm vi tuổi thọ biến đổi để ngăn chặn nó tại thời gian biên dịch. – Jeff

14

Bằng cách này bạn sẽ có được nhiều hơn và không kém.

  • Bạn nhận được cùng: Bạn vẫn có thể mở các tập tin thông qua constructor. Bạn vẫn nhận được RAII: nó sẽ tự động đóng các tập tin lúc phá hủy đối tượng.

  • Bạn nhận được nhiều hơn: bạn có thể sử dụng cùng một luồng để mở lại tệp khác; bạn có thể đóng tập tin khi bạn muốn, không bị giới hạn chờ đợi đối tượng đi ra khỏi phạm vi hoặc bị hủy (điều này rất quan trọng).

  • Bạn nhận được không có gì ít hơn: Lợi thế bạn thấy là không có thật. Bạn nói rằng theo cách của bạn, bạn không phải kiểm tra từng hoạt động. Điều này là sai. Luồng có thể không thành công bất kỳ lúc nào ngay cả khi nó đã mở thành công (tệp).

Kiểm tra lỗi và loại trừ ngoại lệ, xem @PiotrS’s answer. Về mặt khái niệm, tôi thấy không có sự khác biệt giữa việc phải kiểm tra trạng thái trả về và phải bắt lỗi. Lỗi vẫn còn đó; sự khác biệt là cách bạn phát hiện ra nó. Nhưng như được chỉ bởi @PiotrS bạn có thể lựa chọn không cho cả hai.

+0

Mặt nạ 'std :: ios :: exceptions()' cho phép truyền lỗi không được kiểm tra, như được ghi chú bởi Piotr. – Jeff

+2

@ Jeff Tôi đã giải thích câu hỏi này dọc theo các dòng tại sao bạn được phép mở/đóng luồng một cách rõ ràng bất kỳ lúc nào; thay vào đó là lý do tại sao kiểm tra lỗi và không ngoại lệ. – bolov

+1

Nó là nhiều hơn về các khía cạnh mở/đóng, và tôi nghĩ rằng tôi vẫn không đồng ý với bạn nhiều hơn/không có gì xác nhận ít hơn. Làm một khai báo/mở sau đó một kiểm tra hoặc một tờ khai, mặt nạ ngoại lệ, sau đó mở là vụng về hơn ném trên một instantiation thất bại. Tôi cũng đặt câu hỏi liệu có khả năng chuyển hướng một tập tin xử lý thực sự mua bất cứ thứ gì. Chi phí của việc xây dựng/phá hủy xử lý không gian người dùng bị lấn át bởi các cuộc gọi hệ thống cơ bản và việc khử == đóng ngăn chặn một lớp nhỏ các lỗi truy cập câm. – Jeff

4

Thư viện file chuẩn suối làm cung cấp RAII, trong cảm giác rằng cách gọi destructor về ai sẽ đóng bất kỳ tập tin đó sẽ xảy ra là mở. Tuy nhiên, ít nhất trong trường hợp đầu ra, , đây là biện pháp khẩn cấp, chỉ nên sử dụng nếu bạn gặp lỗi khác và sẽ không sử dụng tệp vẫn đang được ghi. (Lập trình tốt thực hành sẽ xóa nó.) Nói chung, bạn cần kiểm tra trạng thái của luồng sau bạn đã đóng cửa và đây là một hoạt động có thể bị lỗi, vì vậy không nên thực hiện các destructor.

Để nhập, nó không quá quan trọng, vì bạn sẽ kiểm tra trạng thái sau lần nhập cuối cùng, và hầu hết thời gian, sẽ đã đọc cho đến khi dữ liệu nhập thất bại. Nhưng nó có vẻ hợp lý để có cùng một giao diện cho cả hai; từ một điểm lập trình của xem, tuy nhiên, bạn thường có thể chỉ để cho đóng trong các destructor làm công việc của mình trên đầu vào.

Liên quan đến open: bạn có thể dễ dàng mở trong hàm tạo và đối với các cách sử dụng riêng lẻ như bạn hiển thị, đây có thể là giải pháp ưa thích là .Nhưng có trường hợp bạn có thể muốn sử dụng lại std::filebuf, mở và đóng một cách rõ ràng, và tất nhiên, trong hầu hết các trường hợp, bạn sẽ muốn xử lý lỗi khi mở tệp ngay lập tức, thay vì thông qua một số ngoại lệ.

1

Tùy thuộc vào những gì bạn đang làm, đọc hoặc viết. Bạn có thể đóng gói luồng đầu vào theo cách RAII, nhưng điều này không đúng đối với luồng đầu ra. Nếu đích là một tập tin đĩa hoặc ổ cắm mạng, KHÔNG BAO GIỜ, KHÔNG BAO GIỜ đặt fclose/đóng trong destructor. Bởi vì bạn cần kiểm tra giá trị trả về của fclose, và không có cách nào để báo cáo lỗi xảy ra trong destructor. xem How can I handle a destructor that fails

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