Neden C ++ 'nihayet' yapı yok?


57

C ++ 'da kural dışı durum işleme, dene / at / yakala ile sınırlıdır. Object Pascal'dan farklı olarak Java, C # ve Python, C ++ 11'de bile finallyyapı uygulanmadı.

"İstisna güvenli kodunu" tartışan çok fazla C ++ literatürü gördüm. Lippman, güvenli kod istisnasının, Primerinin kapsamının ötesinde, önemli ama gelişmiş, zor bir konu olduğunu yazmaktadır - bu, güvenli kodun C ++ için temel olmadığı anlamına gelir. Herb Sutter, istisnai C ++ 'sında konuya 10 bölüm ayırıyor!

Yine de bana, "istisna güvenli kodu" yazmaya çalışırken karşılaşılan sorunların birçoğu, eğer bir finallyyapı uygulanmışsa, programcının bir istisna durumunda bile programın geri yüklenebilmesini sağlamasına olanak tanıyan oldukça iyi bir şekilde çözülebileceğini düşünüyor. Kaynakların tahsisi noktasına ve potansiyel olarak sorunlu kodlara yakın, güvenli, istikrarlı, sızdırmaz bir duruma. Çok tecrübeli bir Delphi ve C # programcısı olarak kullanıyorum deniyorum .. Sonunda kodumda bu kadar çok programcı olduğu gibi kodları oldukça engelliyor.

C ++ 11'de uygulanan tüm 'çan ve ıslık' dikkate alındığında, 'nihayet' hala orada olmadığını bulmaktan şaşırmıştım.

Öyleyse, neden finallyyapı C ++ 'ta hiç uygulanmadı? Kavramak çok zor ve gelişmiş bir kavram değil ve programcının 'istisna güvenli kod' yazmasına yardımcı olmak için uzun bir yol kat ediyor.


25
Neden sonunda hayır? Çünkü yıkıcıdaki nesne (veya akıllı işaretçi) kapsam bıraktığında otomatik olarak çalışan şeyleri serbest bırakırsınız. Yıkıcılar, iş akışını temizleme mantığından ayırdığı için nihayet {} den üstündür. Tıpkı ücretsiz çağrıları yapmak istemeyeceğiniz gibi (iş akışınızı çöp toplanan bir dilde toparlayarak).
mike30


8
“Neden finallyC ++ ' da hayır yok ve istisna yönetimi için hangi teknikleri kullanıyorsunuz?” Sorusunu sormak. bu site için geçerlidir. Bence mevcut cevaplar bunu iyi anlıyor. “C ++ tasarımcılarının finallydeğerli olmama sebepleri var mı?” Konulu bir tartışmaya dönüştürülmesi. ve " finallyC ++ 'a eklenmeli mi?" ve soru hakkındaki yorumlar arasında tartışmaya devam etmek ve her cevap bu soru-cevap sitesinin modeline uymuyor.
Josh Kelley

2
Sonunda, endişelerinizin ayrılışı zaten varsa: ana kod bloğu burada ve temizleme kaygısı burada halledilir.
Kaz,

2
@Kaz. Aradaki fark açık ve net temizliktir. Bir yıkıcı, düz eski bir ilkelin yığından fırlarken nasıl temizlendiğine benzer şekilde otomatik temizleme sağlar. Net bir şekilde aramaları temizlemenize gerek yoktur ve ana mantığınıza odaklanabilirsiniz. Bir denemede / nihayet yığını tahsis edilmiş ilkelleri temizlemeniz gerektiğinde ne kadar iç içe olacağını hayal edin. Örtülü temizlik üstündür. Sınıf sözdiziminin adsız işlevlerle karşılaştırılması önemli değildir. Her ne kadar birinci sınıf işlevleri bir tanıtıcı serbest bırakan bir işleve geçirerek elle temizlemeyi merkezileştirebilir.
mike30

Yanıtlar:


56

@ Nemanja'nın cevabına dair ek yorumların olduğu gibi (bu, Stroustrup'tan alıntı yaptığından beri alabileceğiniz kadar iyi bir cevap):

Gerçekten sadece C ++ felsefesini ve deyimlerini anlama meselesi. Kalıcı bir sınıfta bir veritabanı bağlantısı açan ve bir istisna atılırsa bu bağlantıyı kapattığından emin olmak zorunda olduğunuz bir işlem örneğinize bakın. Bu istisnai bir güvenlik meselesidir ve istisnaları olan herhangi bir dil için geçerlidir (C ++, C #, Delphi ...).

try/ Kullanan bir dilde finally, kod şöyle görünebilir:

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

Basit ve anlaşılır. Bununla birlikte, birkaç dezavantajı vardır:

  • Dil deterministik yıkıcı yoksa, ben her zaman var yazmak için finallyyoksa ben kaynakları sızdırıyor, blok.
  • Eğer DoRiskyOperationtek bir yöntem çağrısı daha fazladır - Ben yapmak için bazı işleme varsa trybloğu - o zaman Closeoperasyon iyi bir bit uzak olma sona erebilir Openoperasyonu. Temizlik işimi kazanımın yanına yazamıyorum.
  • Elde edilmesi gereken birkaç kaynağa sahip olursam, istisnasız bir şekilde serbest bırakılırsa, birkaç blok try/ derinlikte birkaç katmanla sonuçlanabilirim finally.

C ++ yaklaşımı şöyle görünür:

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

Bu, finallyyaklaşımın tüm dezavantajlarını tamamen çözer . Kendi dezavantajları var, ancak göreceli olarak küçük:

  • ScopedDatabaseConnectionDersi kendiniz yazmak için iyi bir şans var . Ancak, çok basit bir uygulama - sadece 4 veya 5 kod satırı.
  • "Dağınıklığını temizlemek için yıkıcılarına güvenmek için sürekli olarak sınıfları oluşturma ve yok etme" hakkındaki yorumuna dayanarak, görünüşe göre hayranı olmadığınız fazladan bir yerel değişken yaratmayı içerir - ama iyi bir derleyici en iyi duruma getirecek Ekstra bir yerel değişkenin içerdiği ekstra çalışmalardan herhangi birini. İyi C ++ tasarımı bu tür optimizasyonlara çok güveniyor.

Şahsen, bu avantaj ve dezavantajları göz önüne alarak, RAII'yi tercih edebileceğim bir teknik buluyorum finally. Kilometreniz değişebilir.

Son olarak, RAII C ++ 'ta çok iyi tanımlanmış bir deyim olduğundan ve bazı geliştiricilerin sayısız Scoped...sınıf yazma yükünü hafiflettiği için, bu tür deterministik temizliği kolaylaştıran ScopeGuard ve Boost.ScopeExit gibi kütüphaneler vardır .


8
C # usingifadesi, IDisposablearayüzü uygulayan herhangi bir nesneyi otomatik olarak temizleyen ifadeye sahiptir . Yani yanlış anlaşmak mümkün olsa da, doğru anlaşılması oldukça kolaydır.
Robert Harvey,

18
Derleyici bir try/finallyyapıya maruz try/finallybırakmadığı için derleyici tarafından bir yapıyla uygulanan bir tasarım deyimi kullanarak, geçici durum değişiminin tersine çevrilmesi için tamamen yeni bir sınıf yazmak zorunda kalmak; deyim tasarım, bir "avantaj" değildir; soyutlama inversiyonunun tanımı budur.
Mason Wheeler

15
@MasonWheeler - Yeni bir sınıf yazmak zorunda olmanın bir avantaj olduğunu söylemedim. Bir dezavantaj dedim. Denge, olsa, ben de ray tercih sahip kullanmak finally. Dediğim gibi, kilometreniz değişebilir.
Josh Kelley

7
@JoshKelley: 'İyi C ++ tasarımı bu tür optimizasyonlara çok güveniyor.' Fazlalık kodlardan gob'lar yazmak ve ardından derleyici optimizasyonuna güvenmek Good Design ?! IMO, iyi tasarımın antitezidir. İyi tasarımın temelleri arasında özlü, kolay okunur bir kod vardır. Korumak için daha az hata ayıklamak için daha az, vs vs vs gerekir DEĞİL kod gobs yazma ve sonra hepsi son yapmak için derleyici güvenerek - IMO o da hiç mantıklı!
Vektör

14
@Mikey: Yani kod bazında yinelenen temizleme kodu (veya temizliğin gerçekleşmesi gerektiği gerçeği) “özlü” ve “kolayca okunabilir” mi? RAII ile bu kodu bir kez yazarsın ve otomatik olarak her yere uygulanır.
Mankarse

54

Gönderen C temin ++ vermez Neden "nihayet" inşa? içinde Bjarne Stroustrup'un C ++ Stil ve Teknik SSS :

C ++ hemen hemen her zaman daha iyi olan bir alternatifi desteklediğinden: "Kaynak edinimi başlatma" tekniği (TC ++ PL3 bölüm 14.4). Temel fikir, yerel bir nesneyle bir kaynağı temsil etmektir, böylece yerel nesnenin yıkıcısı kaynağı serbest bırakacaktır. Bu şekilde, programcı kaynağı serbest bırakmayı unutamaz.


5
Ama bu teknikte C ++ 'a özgü hiçbir şey yok, değil mi? RAII'yi herhangi bir dilde nesneler, yapıcılar ve yıkıcılar ile yapabilirsiniz. Bu harika bir tekniktir, ancak RAII sadece var olan bir finallyyapının Strousup'ın söylediklerine rağmen sonsuza dek sonsuza dek yararsız olduğu anlamına gelmez . "İstisna güvenli kodu" yazmanın C ++ 'da önemli bir şey olduğu gerçeği bunun kanıtıdır. Heck, C # hem yıkıcı hem de kullanılmış finallyve ikisi de kullanılmış.
17'de

28
@Tacroy: C ++, deterministik yıkıcıları olan çok az sayıda ana dilden biridir . C # "yıkıcılar" bu amaç için kullanışsızdır ve RAII'ye sahip olmak için "kullanma" bloklarını elle yazmanız gerekir.
Nemanja Trifunovic

15
@Mikey, "Neden C ++" nihayet "bir yapı sağlamıyor?" doğrudan orada Stroustrup'tan. Daha ne isteyebilirsin? Yani bir neden.

5
@Mikey Kodunuzun iyi davranması konusunda endişe duyuyorsanız, özellikle de kaynakların sızdırılmaması, istisnalar atıldığında endişe duyuyorsanız , istisna emniyeti / istisna emniyet kodu yazmaya çalışıyorsunuz. Sadece buna demiyorsunuz ve mevcut farklı araçlar nedeniyle onu farklı şekilde uyguluyorsunuz. Ancak, C ++ insanlarının istisna emniyetini tartışırken konuştukları tam olarak budur.

19
@Kaz: Yıkıcıdaki temizliği sadece bir kez yaptığımı hatırlamam gerekiyor, o zamandan sonra sadece nesneyi kullanıyorum. Tahsis edilen işlemi her kullandığımda nihayet bloktaki temizliği yapmayı hatırlamam gerekiyor.
Deworde

19

C ++ 'ın olmamasının nedeni C ++' finallyda gerekli olmadığındandır. finallyBir istisna olup olmadığına bakılmaksızın, hemen hemen her zaman bir çeşit temizleme kodu olan bazı kodları çalıştırmak için kullanılır. C ++ 'da, bu temizleme kodu ilgili sınıfın yıkıcısında olmalı ve yıkıcı her zaman tıpkı bir finallyblok gibi çağrılacaktır . Temizlik için yıkıcıyı kullanmanın deyimine RAII denir .

C ++ topluluğu içinde 'kural dışı durum güvenliği' kodu hakkında daha fazla konuşma olabilir, ancak istisnaları olan diğer dillerde de neredeyse eşit derecede önemlidir. 'İstisna güvenli' kodunun tüm amacı, aradığınız işlev / yöntemlerden herhangi birinde bir istisna meydana gelirse, kodunuzun hangi durumda kaldığını düşünmenizdir.
C ++ 'istisna güvenli' kodu biraz daha önemlidir, çünkü C ++ istisna nedeniyle yetim kalan nesnelerin bakımını yapan otomatik çöp koleksiyonuna sahip değildir.

İstisna emniyetinin C ++ topluluğunda daha fazla tartışılmasının nedeni, muhtemelen C ++ 'da neyin yanlış gidebileceği konusunda daha fazla bilgi sahibi olmanızdan kaynaklanmaktadır, çünkü dilde daha az varsayılan güvenlik ağı vardır.


2
Not: Lütfen C ++ 'in deterministik yıkıcılara sahip olduğunu iddia etmeyin. Objektif Pascal / Delphi'nin de deterministik yıkıcıları var, aynı zamanda 'son' u da desteklediğim için, ilk yorumlarımda açıkladığım çok iyi nedenlerden dolayı.
Vektör

13
@Mikey: finallyC ++ standardına ekleme önerisi olmadığından, C ++ topluluğunun the absence of finallybir sorun olarak görmediği sonucuna varmanın güvenli olduğunu düşünüyorum . finallyC ++ 'nın sahip olduğu tutarlı deterministik yıkıma sahip olmayan birçok dilde . Delphi'nin ikisine de sahip olduğunu görüyorum ama tarihinin hangisinin ilk olduğunu bilecek kadar iyi bilmiyorum.
Bart van Ingen Schenau

3
Dephi yığın tabanlı nesneleri desteklemez - yalnızca yığın tabanlı ve yığıntaki nesne referansları. Bu nedenle, 'nihayet' uygun olduğunda yıkıcılar vb.
Vektör

2
C ++ 'ta muhtemelen ihtiyaç duyulmayan çok fazla zorlama var, bu yüzden doğru cevap olamaz.
Kaz

15
Yirmi yıldan uzun bir süredir dili kullandım ve dili kullanan diğer insanlarla çalıştım, "Gerçekten dilin olmasını diliyorum" diyen çalışan bir C ++ programcısıyla hiç karşılaşmadım finally. Daha kolay erişebileceği herhangi bir görevi hatırlayamıyorum.
Robotu

12

Diğerleri RAII'yi çözüm olarak tartıştılar. Mükemmel bir çözüm. Ancak bu, yaygın olarak istenen bir şey olduğundan, neden eklemediklerini gerçekten belirtmiyor finally. Bunun cevabı, C ++ 'nın tasarımı ve geliştirilmesinde daha temeldir: C ++' nın gelişimi boyunca, katılımcılar, diğer özellikler kullanılarak elde edilebilecek tasarım özelliklerinin tanıtımına, büyük miktarda kargaşa olmadan ve özellikle bunun gerektirdiği durumlarda, karşı koymuştur. eski kodun uyumsuz olmasına neden olabilecek yeni anahtar kelimelerin RAII oldukça işlevsel bir alternatif oluşturduğundan finallyve aslında finallyC ++ 11'de kendinize ait bir rulo atabileceğiniz için , bunun için çok az çağrı yapıldı.

Tek yapmanız gereken, Finallyyapıcısına iletilen işlevinde yıkıcı olarak geçen işlevi çağıran bir sınıf oluşturmak . O zaman bunu yapabilirsiniz:

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

    database.doRisky();
}

Ancak çoğu yerel C ++ programcısı, temiz bir şekilde tasarlanmış RAII nesnelerini tercih eder.


3
Lambdada referans yakalama eksik. Finally atEnd([&] () { database.close(); });Ayrıca Olmalı , Aşağıdakilerin daha iyi olacağını hayal ediyorum: { Finally atEnd(...); try {...} catch(e) {...} }(Sonlandırıcıyı try bloğundan kaldırdım, bu yüzden catch bloklarından sonra çalışacak.)
Thomas Eding 22:14

2

Bir "tuzak" deseni kullanabilirsiniz - try / catch bloğunu kullanmak istemeseniz bile.

Gerekli kapsamda basit bir nesne koyun. Bu nesnenin yıkıcısına "son" mantığını koy. Ne olursa olsun, yığın çözülmediğinde, nesnenin yıkıcısı çağrılır ve şekerinizi alırsınız.


1
Bu soruyu cevaplamıyor ve basitçe nihayetinde sonuçta böyle kötü bir fikir olmadığını kanıtlıyor ...
Vector

2

Eh, finallyderlemek için aşağıdakini alabilen Lambdas kullanarak kendi türünüzü kullanabilirsiniz (en güzel kod parçasını değil, RAII'siz bir örnek kullanarak):

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

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

Bu yazıya bakınız .


-2

RAII'nin bir süperset olduğu iddiasıyla aynı fikirde olduğuma emin değilim finally. RAII'nin aşil topuğu basittir: istisnalar. RAII, yıkıcılar ile uygulanır ve bir yıkıcıyı atmak her zaman C ++ 'ta yanlıştır. Bu, temizleme kodunuza atmak istediğinizde RAII kullanamayacağınız anlamına gelir. Eğer finallyuygulanmıştır, diğer taraftan, bir den atmak yasal olmaz inanmak için hiçbir neden yok finallyblokta.

Bunun gibi bir kod yolunu düşünün:

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;
    }
}

Biz olsaydı finallybiz yazabiliriz:

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();
    }
}

Ancak, RAII kullanarak eşdeğer davranışı elde etmenin bir yolu yoktur.

Birisi bunu C ++ 'da nasıl yapacağını biliyorsa, cevabı çok merak ediyorum. Örneğin, güvenilen bir şeyden memnun olurum, örneğin, tüm istisnaların özel bir yetenek veya başka bir şeyle tek bir sınıftan miras almasını sağlamaktan.


1
İkinci örneğinizde, eğer complex_cleanupfırlatabilirseniz, o zaman, yakalanmamış iki istisnanın aynı anda uçuşta olduğu bir durumda olabilirsiniz, tıpkı RAII / yıkıcılar gibi, ve C ++ buna izin vermeyi reddeder. Orijinal istisnanın complex_cleanupgörünmesini istiyorsanız, RAII / yıkıcılar ile olduğu gibi, istisnaları da engellemelisiniz. İsterseniz complex_cleanupgörülecek 'ın istisna, o zaman iç içe deneyin / catch bloğu kullanabilirsiniz düşünüyorum - bu teğet ve bir yorum sığmayacak zor olsa da, bu yüzden ayrı bir soru değer.
Josh Kelley,

İlk örnek olarak aynı davranışı elde etmek için daha güvenli bir şekilde RAII kullanmak istiyorum. Varsayılan bir finallybloğa yapılan bir atış açıkça catchWRT uçuş dışı istisnalar içindeki bir bloktaki fırlatma ile aynı şekilde çalışır - çağrı yapmaz std::terminate. Soru "neden finallyC ++ 'da hayır ?" ve cevapların hepsi "İhtiyacın yok ... RAII FTW!" Demek istediğim evet, RAII bellek yönetimi gibi basit durumlar için gayet iyi, ancak istisnalar çözülene kadar genel amaçlı bir çözüm olarak çok fazla düşünce / ek yük / endişe / yeniden tasarım gerektiriyor.
MadScientist

3
Amacınızı anlıyorum - yıkıcılarla atılabilecek bazı meşru sorunlar var - ama bunlar nadir. RAII + istisnalarının çözülmemiş sorunları olduğunu veya RAII'nin genel amaçlı bir çözüm olmadığını söylemek, çoğu C ++ geliştiricisinin deneyimiyle eşleşmiyor.
Josh Kelley

1
Kendinizi yıkıcılarda istisnalar ortaya koyma ihtiyacı ile bulursanız, yanlış bir şey yapıyorsunuz - muhtemelen gerekli olmadığında diğer yerlerde işaretçiler kullanıyorsunuz.
Vektör

1
Bu yorumlar için çok fazla söz konusu. Bununla ilgili bir soru sorun: Bu senaryoyu, RAII modelini kullanarak C ++ 'ta nasıl ele alırsınız ... işe yaramadı ... Yine, yorumlarınızı yönlendirmelisiniz : konuştuğunuz üyenin @ yazın ve üyenin adı Yorumunuzun başında. Yorumlar kendi gönderinizdeyken, her şeyden haberdar olursunuz, ancak başkaları onlara yorum yapmadıkça söylemez.
Vektör
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.