Tại sao không có cấu trúc 'cuối cùng' trong C ++?


57

Xử lý ngoại lệ trong C ++ bị giới hạn ở thử / ném / bắt. Không giống như Object Pascal, Java, C # và Python, ngay cả trong C ++ 11, finallycấu trúc đã không được thực hiện.

Tôi đã thấy rất nhiều tài liệu C ++ bàn về "mã an toàn ngoại lệ". Lippman viết rằng mã an toàn ngoại lệ là một chủ đề quan trọng nhưng tiên tiến, khó, vượt ra ngoài phạm vi của Primer của anh ta - điều này dường như ngụ ý rằng mã an toàn không phải là nền tảng của C ++. Herb Sutter dành 10 chương cho chủ đề trong C ++ đặc biệt của mình!

Tuy nhiên, dường như nhiều vấn đề gặp phải khi cố gắng viết "mã an toàn ngoại lệ" có thể được giải quyết khá tốt nếu finallycấu trúc được triển khai, cho phép lập trình viên đảm bảo rằng ngay cả trong trường hợp ngoại lệ, chương trình có thể được khôi phục đến trạng thái an toàn, ổn định, không rò rỉ, gần với điểm phân bổ tài nguyên và mã có khả năng có vấn đề. Là một lập trình viên Delphi và C # rất có kinh nghiệm, tôi sử dụng thử .. cuối cùng cũng chặn khá nhiều mã của tôi, cũng như hầu hết các lập trình viên trong các ngôn ngữ này.

Xem xét tất cả các 'chuông và còi' được triển khai trong C ++ 11, tôi đã rất ngạc nhiên khi thấy rằng 'cuối cùng' vẫn không có ở đó.

Vậy, tại sao finallycấu trúc chưa bao giờ được thực hiện trong C ++? Đây thực sự không phải là một khái niệm quá khó hoặc tiên tiến để nắm bắt và đi một chặng đường dài hướng tới việc giúp lập trình viên viết 'mã an toàn ngoại lệ'.


25
Tại sao cuối cùng không có? Bởi vì bạn giải phóng những thứ trong hàm hủy sẽ tự động kích hoạt khi đối tượng (hoặc con trỏ thông minh) rời khỏi phạm vi. Công cụ phá hủy vượt trội hơn so với cuối cùng {} vì nó tách luồng công việc khỏi logic dọn dẹp. Giống như bạn sẽ không muốn các cuộc gọi đến () làm xáo trộn quy trình làm việc của mình bằng ngôn ngữ được thu gom rác.
mike30


8
Đặt câu hỏi, "Tại sao không có finallytrong C ++ và những kỹ thuật xử lý ngoại lệ nào được sử dụng ở vị trí của nó?" là hợp lệ và về chủ đề cho trang web này. Các câu trả lời hiện có bao gồm điều này tốt, tôi nghĩ. Biến nó thành một cuộc thảo luận về "Những lý do của các nhà thiết kế C ++ không bao gồm finallygiá trị?" và "Có nên finallythêm vào C ++ không?" và tiếp tục thảo luận về các bình luận về câu hỏi và mọi câu trả lời không phù hợp với mô hình của trang web Hỏi & Đáp này.
Josh Kelley

2
Nếu cuối cùng, bạn đã có sự phân tách các mối quan tâm: khối mã chính ở đây và mối quan tâm dọn dẹp được quan tâm ở đây.
Kaz

2
@Kaz. Sự khác biệt là ngầm vs dọn dẹp rõ ràng. Một hàm hủy cho phép bạn dọn dẹp tự động tương tự như cách một nguyên thủy cũ đơn giản được dọn sạch khi nó bật ra khỏi ngăn xếp. Bạn không cần thực hiện bất kỳ cuộc gọi dọn dẹp rõ ràng nào và có thể tập trung vào logic cốt lõi của mình. Hãy tưởng tượng nó sẽ phức tạp như thế nào nếu bạn phải dọn sạch các nguyên thủy được phân bổ trong một lần thử / cuối cùng. Dọn dẹp sạch sẽ là vượt trội. Việc so sánh cú pháp lớp với các hàm ẩn danh không liên quan. Mặc dù bằng cách chuyển các chức năng hạng nhất cho một chức năng giải phóng một tay cầm có thể tập trung dọn dẹp thủ công.
mike30

Câu trả lời:


57

Như một số bình luận bổ sung về câu trả lời của @ Nemanja (trong đó, vì nó trích dẫn Stroustrup, thực sự là một câu trả lời tốt như bạn có thể nhận được):

Đó thực sự chỉ là vấn đề hiểu triết lý và thành ngữ của C ++. Lấy ví dụ của bạn về một hoạt động mở kết nối cơ sở dữ liệu trên một lớp liên tục và phải đảm bảo rằng nó đóng kết nối đó nếu ném ngoại lệ. Đây là vấn đề an toàn ngoại lệ và áp dụng cho bất kỳ ngôn ngữ nào có ngoại lệ (C ++, C #, Delphi ...).

Trong ngôn ngữ sử dụng try/ finally, mã có thể trông giống như thế này:

database.Open();
try {
    database.DoRiskyOperation();
} finally {
    database.Close();
}

Đơn giản và dễ hiểu. Tuy nhiên, có một vài nhược điểm:

  • Nếu ngôn ngữ không có hàm hủy xác định, tôi luôn phải viết finallykhối, nếu không tôi sẽ rò rỉ tài nguyên.
  • Nếu DoRiskyOperationnhiều hơn một cuộc gọi phương thức duy nhất - nếu tôi có một số xử lý phải thực hiện trong trykhối - thì Closehoạt động có thể kết thúc cách xa Openhoạt động một chút . Tôi không thể viết dọn dẹp của tôi ngay bên cạnh việc mua lại của tôi.
  • Nếu tôi có một số tài nguyên cần phải mua thì được giải phóng theo cách an toàn ngoại lệ, tôi có thể kết thúc với một vài lớp sâu try/ finallykhối.

Cách tiếp cận C ++ sẽ như thế này:

ScopedDatabaseConnection scoped_connection(database);
database.DoRiskyOperation();

Điều này hoàn toàn giải quyết tất cả các nhược điểm của finallyphương pháp này. Nó có một vài nhược điểm của riêng nó, nhưng chúng tương đối nhỏ:

  • Có một cơ hội tốt bạn cần phải tự viết ScopedDatabaseConnectionlớp. Tuy nhiên, đó là một cách thực hiện rất đơn giản - chỉ có 4 hoặc 5 dòng mã.
  • Nó liên quan đến việc tạo thêm một biến cục bộ - mà bạn dường như không phải là người hâm mộ, dựa trên nhận xét của bạn về việc "liên tục tạo và hủy các lớp để dựa vào các hàm hủy của chúng để dọn dẹp mớ hỗn độn của bạn là rất kém" - nhưng một trình biên dịch tốt sẽ tối ưu hóa ra bất kỳ công việc bổ sung nào mà một biến cục bộ phụ liên quan. Thiết kế C ++ tốt phụ thuộc rất nhiều vào các loại tối ưu hóa này.

Cá nhân, xem xét những lợi thế và bất lợi này, tôi thấy RAII là một kỹ thuật thích hợp hơn nhiều finally. Số dặm của bạn có thể thay đổi.

Cuối cùng, vì RAII là một thành ngữ được thiết lập tốt trong C ++ và để giảm bớt các nhà phát triển về một số gánh nặng của việc viết nhiều Scoped...lớp, có các thư viện như ScopeGuardBoost.ScopeExit tạo điều kiện cho việc dọn dẹp xác định này.


8
C # có usingcâu lệnh, tự động dọn sạch mọi đối tượng thực hiện IDisposablegiao diện. Vì vậy, trong khi có thể hiểu sai, thật dễ dàng để làm cho đúng.
Robert Harvey

18
Phải viết một lớp hoàn toàn mới để xử lý sự đảo ngược thay đổi trạng thái tạm thời, sử dụng một thành ngữ thiết kế được trình biên dịch triển khai với một try/finallycấu trúc vì trình biên dịch không phơi bày một try/finallycấu trúc và cách duy nhất để truy cập nó là thông qua lớp thành ngữ thiết kế, không phải là một "lợi thế;" đó là định nghĩa của một sự đảo ngược trừu tượng.
Mason Wheeler

15
@MasonWheeler - Umm, tôi không nói rằng phải viết một lớp mới là một lợi thế. Tôi nói đó là một bất lợi. Mặc dù vậy, về số dư, tôi thích RAII hơn là phải sử dụng finally. Như tôi đã nói, số dặm của bạn có thể thay đổi.
Josh Kelley

7
@JoshKelley: 'Thiết kế C ++ tốt phụ thuộc rất nhiều vào các loại tối ưu hóa này.' Viết gobs của mã ngoại lai và sau đó dựa vào tối ưu hóa trình biên dịch là Thiết kế tốt ?! IMO đó là phản đề của thiết kế tốt. Trong số các nguyên tắc cơ bản của thiết kế tốt là mã ngắn gọn, dễ đọc. Ít hơn để gỡ lỗi, ít hơn để duy trì, vv vv vv Bạn nên KHÔNG được viết gobs của mã và sau đó dựa vào trình biên dịch để làm cho nó tất cả đi - IMO mà làm cho không có ý nghĩa gì cả!
Vector

14
@Mikey: Vì vậy, sao chép mã dọn dẹp (hoặc thực tế là việc dọn dẹp phải xảy ra) trên toàn bộ cơ sở mã là "súc tích" và "dễ đọc"? Với RAII, bạn viết mã như vậy một lần và nó được tự động áp dụng ở mọi nơi.
Nhân

55

Từ Tại sao C ++ không cung cấp cấu trúc "cuối cùng"? trong Câu hỏi thường gặp về Phong cách và Kỹ thuật C ++ của Bjarne Stroustrup :

Bởi vì C ++ hỗ trợ một giải pháp thay thế gần như luôn luôn tốt hơn: Kỹ thuật "thu nhận tài nguyên là khởi tạo" (TC ++ PL3 phần 14.4). Ý tưởng cơ bản là biểu diễn một tài nguyên bởi một đối tượng cục bộ, để hàm hủy của đối tượng cục bộ sẽ giải phóng tài nguyên. Bằng cách đó, lập trình viên không thể quên giải phóng tài nguyên.


5
Nhưng không có gì về kỹ thuật đó dành riêng cho C ++, phải không? Bạn có thể thực hiện RAII bằng bất kỳ ngôn ngữ nào với các đối tượng, hàm tạo và hàm hủy. Đó là một kỹ thuật tuyệt vời, nhưng RAII chỉ tồn tại không ngụ ý rằng một finallycông trình luôn vô dụng mãi mãi, bất chấp những gì Strousup đang nói. Thực tế là việc viết "mã an toàn ngoại lệ" là một vấn đề lớn trong C ++ là bằng chứng cho điều đó. Heck, C # có cả destructors và finally, và họ đều quen.
Tacroy

28
@Tacroy: C ++ là một trong số rất ít ngôn ngữ chính có các hàm hủy xác định . "Kẻ hủy diệt" C # là vô dụng cho mục đích này và bạn cần phải viết thủ công các khối "sử dụng" để có RAII.
Nemanja Trifunovic

15
@Mikey bạn có câu trả lời là "Tại sao C ++ không cung cấp cấu trúc" cuối cùng "?" trực tiếp từ Stroustrup mình ở đó. Nhiều hơn những gì bạn có thể yêu cầu? Đó lý do tại sao.

5
@Mikey Nếu bạn lo lắng về mã của bạn cư xử tốt, đặc biệt là nguồn tài nguyên không bị rò rỉ, khi trường hợp ngoại lệ được ném vào nó, bạn đang lo lắng về sự an toàn ngoại lệ / cố gắng để viết ngoại lệ đang an toàn. Bạn chỉ không gọi nó như vậy, và do các công cụ khác nhau có sẵn, bạn thực hiện nó khác nhau. Nhưng đó chính xác là những gì người C ++ nói về khi họ thảo luận về an toàn ngoại lệ.

19
@Kaz: Tôi chỉ cần nhớ thực hiện dọn dẹp trong hàm hủy một lần, và từ đó tôi chỉ sử dụng đối tượng. Tôi cần nhớ thực hiện dọn dẹp trong khối cuối cùng mỗi khi tôi sử dụng thao tác phân bổ.
deworde

19

Lý do mà C ++ không có finallylà vì nó không cần thiết trong C ++. finallyđược sử dụng để thực thi một số mã bất kể có xảy ra ngoại lệ hay không, mà hầu như luôn luôn là một loại mã dọn dẹp. Trong C ++, mã dọn dẹp này phải nằm trong hàm hủy của lớp có liên quan và hàm hủy sẽ luôn được gọi, giống như một finallykhối. Thành ngữ của việc sử dụng hàm hủy để dọn dẹp của bạn được gọi là RAII .

Trong cộng đồng C ++, có thể nói nhiều hơn về mã 'ngoại lệ an toàn', nhưng nó hầu như không kém phần quan trọng trong các ngôn ngữ khác có ngoại lệ. Toàn bộ điểm của mã 'ngoại lệ an toàn' là bạn nghĩ về trạng thái mà mã của bạn bị bỏ lại nếu một ngoại lệ xảy ra trong bất kỳ chức năng / phương thức nào bạn gọi.
Trong C ++, mã 'ngoại lệ an toàn' quan trọng hơn một chút, vì C ++ không có bộ sưu tập rác tự động chăm sóc các đối tượng bị bỏ rơi mồ côi do một ngoại lệ.

Lý do an toàn ngoại lệ được thảo luận nhiều hơn trong cộng đồng C ++ có lẽ cũng xuất phát từ thực tế là trong C ++, bạn phải nhận thức rõ hơn về những gì có thể sai, bởi vì ngôn ngữ có ít mạng lưới an toàn mặc định hơn.


2
Lưu ý: Xin đừng cho rằng C ++ có các hàm hủy xác định. Đối tượng Pascal / Delphi cũng có các hàm hủy xác định nhưng cũng hỗ trợ 'cuối cùng', vì những lý do rất chính đáng mà tôi đã giải thích trong các bình luận đầu tiên của mình bên dưới.
Vector

13
@Mikey: Cho rằng chưa bao giờ có đề xuất bổ sung finallyvào tiêu chuẩn C ++, tôi nghĩ sẽ an toàn khi kết luận rằng cộng đồng C ++ không coi là the absence of finallycó vấn đề. Hầu hết các ngôn ngữ không có finallysự phá hủy xác định nhất quán mà C ++ có. Tôi thấy rằng Delphi có cả hai, nhưng tôi không biết rõ về lịch sử của nó đủ để biết cái nào có trước.
Bart van Ingen Schenau

3
Dephi không hỗ trợ các đối tượng dựa trên ngăn xếp - chỉ dựa trên heap và các tham chiếu đối tượng trên ngăn xếp. Do đó, 'cuối cùng' là cần thiết để gọi rõ ràng các hàm hủy, v.v. khi thích hợp.
Vector

2
Có rất nhiều hành trình trong C ++ được cho là không cần thiết, vì vậy đây không thể là câu trả lời đúng.
Kaz

15
Trong hơn hai thập kỷ tôi đã sử dụng ngôn ngữ này và làm việc với những người khác sử dụng ngôn ngữ này, tôi chưa bao giờ gặp phải một lập trình viên C ++ đang làm việc nói rằng "Tôi thực sự ước ngôn ngữ này có finally". Tôi không bao giờ có thể nhớ lại bất kỳ nhiệm vụ nào mà nó sẽ trở nên dễ dàng hơn, nếu tôi có quyền truy cập vào nó.
Gort Robot

12

Những người khác đã thảo luận về RAII là giải pháp. Đó là một giải pháp hoàn toàn tốt. Nhưng điều đó không thực sự giải quyết tại sao họ không thêm finallyvào vì đó là điều mong muốn rộng rãi. Câu trả lời cho điều đó là cơ bản hơn cho thiết kế và phát triển C ++: trong suốt quá trình phát triển C ++, những người liên quan đã phản đối mạnh mẽ việc giới thiệu các tính năng thiết kế có thể đạt được bằng cách sử dụng các tính năng khác mà không cần nhiều sự ồn ào và đặc biệt là điều này đòi hỏi phải giới thiệu từ khóa mới có thể khiến mã cũ không tương thích. Vì RAII cung cấp một giải pháp thay thế có chức năng cao finallyvà bạn thực sự có thể tự quay finallytrong C ++ 11, nên có rất ít lời kêu gọi.

Tất cả những gì bạn cần làm là tạo một lớp Finallygọi hàm được truyền cho hàm tạo của nó trong hàm hủy của nó. Sau đó, bạn có thể làm điều này:

try
{
    Finally atEnd([&] () { database.close(); });

    database.doRisky();
}

Tuy nhiên, hầu hết các lập trình viên C ++ bản địa sẽ thích các đối tượng RAII được thiết kế sạch sẽ.


3
Bạn đang thiếu chụp tham chiếu trong lambda của bạn. Finally atEnd([&] () { database.close(); });Cũng nên , tôi tưởng tượng như sau là tốt hơn: { Finally atEnd(...); try {...} catch(e) {...} }(Tôi nhấc bộ hoàn thiện ra khỏi khối thử để nó thực thi sau các khối bắt.)
Thomas Eding

2

Bạn có thể sử dụng mẫu "bẫy" - ngay cả khi bạn không muốn sử dụng khối thử / bắt.

Đặt một đối tượng đơn giản trong phạm vi yêu cầu. Trong hàm hủy của đối tượng này đặt logic "cuối cùng" của bạn. Dù thế nào đi chăng nữa, khi ngăn xếp không được giải quyết, kẻ hủy diệt đối tượng sẽ được gọi và bạn sẽ nhận được kẹo của mình.


1
Điều này không trả lời được câu hỏi, và đơn giản chứng minh rằng cuối cùng thì đó không phải là một ý tưởng tồi như vậy ...
Vector

2

Chà, bạn có thể sắp xếp cuộn của riêng mình finally, sử dụng Lambdas, cái mà sẽ có được những thứ sau đây để biên dịch tốt (dĩ nhiên sử dụng một ví dụ không có RAII, không phải là đoạn mã đẹp nhất):

{
    FILE *file = fopen("test","w");

    finally close_the_file([&]{
        cout << "We're closing the file in a pseudo-finally clause." << endl;
        fclose(file);
    });
}

Xem bài viết này .


-2

Tôi không chắc chắn tôi đồng ý với các xác nhận ở đây rằng RAII là một siêu thay thế finally. Gót chân achilles của RAII rất đơn giản: ngoại lệ. RAII được triển khai với các hàm hủy và C ++ luôn luôn sai khi loại bỏ hàm hủy. Điều đó có nghĩa là bạn không thể sử dụng RAII khi bạn cần mã dọn dẹp. Nếu finallyđược thực hiện, mặt khác, không có lý do gì để tin rằng việc ném từ một finallykhối sẽ không hợp pháp .

Hãy xem xét một đường dẫn mã như thế này:

void foo() {
    try {
        ... stuff ...
        complex_cleanup();
    } catch (A& a) {
        handle_a(a);
        complex_cleanup();
        throw;
    } catch (B& b) {
        handle_b(b);
        complex_cleanup();
        throw;
    } catch (...) {
        handle_generic();
        complex_cleanup();
        throw;
    }
}

Nếu chúng ta có finallychúng ta có thể viết:

void foo() {
    try {
        ... stuff ...
    } catch (A& a) {
        handle_a(a);
        throw;
    } catch (B& b) {
        handle_b(b);
        throw;
    } catch (...) {
        handle_generic();
        throw;
    } finally {
        complex_cleanup();
    }
}

Nhưng không có cách nào, mà tôi có thể tìm thấy, để có được hành vi tương đương bằng RAII.

Nếu ai đó biết cách làm điều này trong C ++, tôi rất thích câu trả lời. Tôi thậm chí còn hài lòng với một cái gì đó dựa vào, ví dụ, thực thi rằng tất cả các ngoại lệ được thừa hưởng từ một lớp duy nhất với một số khả năng đặc biệt hoặc một cái gì đó.


1
Trong ví dụ thứ hai của bạn, nếu complex_cleanupcó thể ném, thì bạn có thể gặp trường hợp hai trường hợp ngoại lệ chưa được phát hiện cùng một lúc, giống như bạn làm với RAII / tàu khu trục và C ++ từ chối cho phép điều này. Nếu bạn muốn nhìn thấy ngoại lệ ban đầu, thì complex_cleanupnên ngăn chặn mọi ngoại lệ, giống như với RAII / hàm hủy. Nếu bạn muốn complex_cleanupthấy ngoại lệ của mình, thì tôi nghĩ bạn có thể sử dụng các khối try / Catch lồng nhau - mặc dù đây là một tiếp tuyến và khó phù hợp với một nhận xét, vì vậy nó đáng để đặt câu hỏi riêng.
Josh Kelley

Tôi muốn sử dụng RAII để có được hành vi giống hệt như ví dụ đầu tiên, an toàn hơn. Việc ném vào một finallykhối giả định rõ ràng sẽ có tác dụng tương tự như ném vào catchkhối ngoại lệ trong chuyến bay WRT - không gọi std::terminate. Câu hỏi là "tại sao không có finallytrong C ++?" và tất cả các câu trả lời đều nói "bạn không cần nó ... RAII FTW!" Quan điểm của tôi là có, RAII vẫn ổn đối với các trường hợp đơn giản như quản lý bộ nhớ, nhưng cho đến khi vấn đề ngoại lệ được giải quyết, nó đòi hỏi quá nhiều suy nghĩ / chi phí / quan tâm / thiết kế lại để trở thành một giải pháp cho mục đích chung.
MadSellectist

3
Tôi hiểu quan điểm của bạn - có một số vấn đề chính đáng với các kẻ hủy diệt có thể ném - nhưng đó là những vấn đề hiếm gặp. Nói rằng các ngoại lệ RAII + có vấn đề chưa được giải quyết hoặc RAII không phải là một giải pháp cho mục đích chung đơn giản là không phù hợp với kinh nghiệm của hầu hết các nhà phát triển C ++.
Josh Kelley

1
Nếu bạn thấy mình có nhu cầu đưa ra ngoại lệ trong các hàm hủy, bạn đang làm gì đó sai - có thể sử dụng con trỏ ở những nơi khác khi không cần thiết.
Vector

1
Điều này là quá tham gia cho ý kiến. Đăng câu hỏi về nó: Bạn sẽ xử lý tình huống này trong C ++ như thế nào bằng mô hình RAII ... nó dường như không hoạt động ... Một lần nữa, bạn nên gửi ý kiến ​​của mình : gõ @ và tên của thành viên bạn đang nói để bắt đầu bình luận của bạn. Khi bình luận trên bài đăng của riêng bạn, bạn sẽ nhận được thông báo về mọi thứ, nhưng những người khác thì không trừ khi bạn gửi bình luận cho họ.
Vector
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.