C ++ có hỗ trợ các khối ' cuối cùng ' không?
Là gì thành ngữ RAII ?
Sự khác biệt giữa thành ngữ RAII của C ++ và câu lệnh 'sử dụng' của C # là gì?
C ++ có hỗ trợ các khối ' cuối cùng ' không?
Là gì thành ngữ RAII ?
Sự khác biệt giữa thành ngữ RAII của C ++ và câu lệnh 'sử dụng' của C # là gì?
Câu trả lời:
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.
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.
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.
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
// 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.
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ới và xó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"; }
FinalAction
về cơ bản giống như ScopeGuard
thành ngữ phổ biến , chỉ với một tên khác.
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.
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.
CleanupFailedException
. Có cách nào hợp lý để đạt được kết quả như vậy khi sử dụng RAII không?
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ệ.
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(); });
...
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ớ.
new
không trả lại NULL, thay vào đó, nó ném một ngoại lệ
std::shared_ptr
và std::unique_ptr
trực tiếp trong stdlib.
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ù.
Như đã chỉ ra trong các câu trả lời khác, C ++ có thể hỗ trợ finally
chứ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, finally
khuyế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.
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 return
bên trong các khối thử & bắt.
std::exception_ptr e; try { /*try block*/ } catch (...) { e = std::current_exception(); } /*finally block*/ if (e) std::rethrow_exception(e);
finally
khối.
Tôi đã đưa ra một finally
macro có thể được sử dụng gần như ¹ finally
từ khóa trong Java; nó sử dụng std::exception_ptr
và bạn bè, các hàm lambda và std::promise
, do đó, nó đòi hỏi C++11
hoặ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::promise
là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:
break
tuyên bố từ bên trong try
và catch()
's khối, vì họ sống trong một hàm lambda;catch()
khối sau try
: đó là yêu cầu C ++;try
và catch()'s
khối, quá trình biên dịch sẽ thất bại vì finally
macro 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_noreturn
macro 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ó. :)
catch(xxx) {}
khối không thể vào lúc bắt đầu finally
macro, 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.
catch(...)
, phải không?
xxx
trong một không gian tên riêng sẽ không bao giờ được sử dụng.
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 downstream
vì 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);
}
}
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
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();
}
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.)
try
{
...
goto finally;
}
catch(...)
{
...
goto finally;
}
finally:
{
...
}
finally
này không làm.