Sorunuz, "İstisna güvenli kod yazmanın çok zor olduğunu" iddia ediyor. Önce sorularınızı, sonra da arkasındaki gizli soruyu cevaplayacağım.
Soruları cevaplama
Gerçekten istisna güvenlik kodu yazıyor musunuz?
Tabii ki biliyorum.
Bu Java C ++ programcısı (RAII semantik eksikliği) olarak benim için cazibesini kaybetti sebebi ama digressing ediyorum: Bu C ++ sorudur.
Aslında, STL veya Boost koduyla çalışmanız gerektiğinde gereklidir. Örneğin, C ++ iş parçacıkları ( boost::thread
veya std::thread
) zarif bir şekilde çıkmak için bir istisna atar.
Son "üretime hazır" kodunuzun istisna güvenli olduğundan emin misiniz?
Bundan emin olabilir misin?
İstisna güvenli kod yazmak, hatasız kod yazmak gibidir.
Kodunuzun istisna güvenli olduğundan% 100 emin olamazsınız. Ama sonra, iyi bilinen kalıpları kullanarak ve iyi bilinen anti-kalıplardan kaçınmak için çaba gösteriyorsunuz.
Çalışan alternatifleri biliyor ve / veya gerçekten kullanıyor musunuz?
Orada hiçbir C uygun alternatifler ++ (eğer C ve önlemek C ++ kütüphaneleri yanı sıra, Windows SEH gibi dış sürprizler geri dönmek gerekir yani).
Özel durum güvenlik kodu yazma
İstisna güvenlik kodu yazmak için, önce yazdığınız her talimatın istisna güvenlik düzeyinin ne olduğunu bilmelisiniz .
Örneğin, a new
bir istisna atabilir, ancak yerleşik (örneğin int veya işaretçi) atamak başarısız olmaz. Bir takas asla başarısız olmaz (asla atma takas yazma), bir std::list::push_back
kutu atma ...
İstisna garantisi
Anlamanız gereken ilk şey, tüm işlevleriniz tarafından sunulan istisna garantisini değerlendirebilmenizdir:
- none : Kodunuz asla bunu sunmamalıdır. Bu kod her şeyi sızdırır ve atılan ilk istisnada bozulur.
- temel : Bu, en azından teklif etmeniz gereken garantidir, yani bir istisna atılırsa, hiçbir kaynak sızdırılmaz ve tüm nesneler hala bütündür
- strong : İşlem başarılı olur veya bir istisna atar, ancak atarsa, veriler işlemin hiç başlamamış olmasıyla aynı durumda olur (bu, C ++ 'ya işlem gücü sağlar)
- nothrow / nofail : İşlem başarılı olacak.
Kod örneği
Aşağıdaki kod doğru C ++ gibi görünüyor, ancak gerçekte "none" garantisi sunuyor ve bu nedenle doğru değil:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Tüm kodumu bu tür analizleri akılda tutarak yazıyorum.
Sunulan en düşük garanti temeldir, ancak daha sonra her komutun sıralaması tüm işlevi "none" yapar, çünkü 3. atarsa x sızıntı yapar.
Yapılacak ilk şey, listeyi güvenli bir şekilde sahip oluncaya kadar x'i akıllı bir işaretçiye koyarak "temel" işlevini yapmak olacaktır:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Şimdi, kodumuz "temel" bir garanti sunuyor. Hiçbir şey sızdırmaz ve tüm nesneler doğru durumda olur. Ama daha fazlasını, yani güçlü garantiyi sunabiliriz. O burasıdır olabilir masraflı hale gelir ve bu yüzden tüm C ++ kod güçlüdür. Hadi deneyelim:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
Operasyonları yeniden sipariş ettik, önce X
doğru değeri yaratıp ayarladık . Herhangi bir işlem başarısız olursa, t
değiştirilmez, bu nedenle 1'den 3'e kadar işlem "güçlü" olarak kabul edilebilir: Bir şey atarsa, t
değiştirilmez ve X
akıllı işaretçiye ait olduğu için sızmaz.
Sonra, bir kopyasını oluşturmak t2
ait t
bir şey atarsa, 7'ye operasyon 4'ten bu kopya üzerinde ve çalışmalarını t2
değiştirilir, ancak daha sonra, t
hala orijinaldir. Hala güçlü garanti veriyoruz.
Sonra, takas t
ve t2
. Takas işlemleri C ++ 'da nothrow olmamalıdır, bu yüzden yazdığınız T
takasın nothrow olmadığını umalım (eğer değilse, yeniden yazmamak için yeniden yazın).
Yani, fonksiyonun sonuna ulaşırsak, her şey başarılı oldu (bir dönüş tipine gerek yok) ve t
istisna değeri var. Başarısız olursa, t
hala orijinal değerine sahiptir.
Şimdi, güçlü garantiyi sunmak oldukça maliyetli olabilir, bu yüzden tüm kodunuza güçlü garanti sunmaya çalışmayın, ancak bunu bir maliyet olmadan yapabilirsiniz (ve C ++ satır içi ve diğer optimizasyon yukarıdaki kodları maliyetsiz yapabilir) , o zaman yap. İşlev kullanıcısı bunun için size teşekkür edecektir.
Sonuç
İstisna güvenli kod yazmak biraz alışkanlık gerektirir. Kullanacağınız her talimatın sunduğu garantiyi değerlendirmeniz ve ardından talimatlar listesinin sunduğu garantiyi değerlendirmeniz gerekir.
Tabii ki, C ++ derleyicisi garantiyi yedeklemeyecek (benim kodumda, garantiyi @ uyarıcı bir oksijen etiketi olarak sunuyoruz), ki bu biraz üzücü, ancak istisna güvenli kod yazmaya çalışmanızı engellememelidir.
Normal hataya karşı hata
Bir programcı, hatasız bir işlevin her zaman başarılı olacağını nasıl garanti edebilir? Sonuçta, işlevin bir hatası olabilir.
Bu doğru. İstisna garantilerinin hatasız kodla sunulması beklenir. Ancak, herhangi bir dilde, bir işlevi çağırmak, işlevin hatasız olduğunu varsayar. Hiçbir aklı başında kod, bir hataya sahip olma olasılığına karşı kendini korumaz. Kodu yapabildiğiniz en iyi şekilde yazın ve ardından hatasız olduğu varsayımıyla garanti verin. Ve bir hata varsa, düzeltin.
İstisnalar, kod hataları için değil, istisnai işlem hataları içindir.
Son sözler
Şimdi, soru "Buna değer mi?"
Tabiki öyle. İşlevin başarısız olmayacağını bilen bir "nothrow / no-fail" fonksiyonuna sahip olmak büyük bir nimettir. Aynı şey, veritabanları gibi işlemsel semantiklerle, kodlama / geri alma özelliklerine sahip kod yazmanıza olanak tanıyan "güçlü" bir işlev için de söylenebilir;
O zaman, "temel" sunmanız gereken en az garantidir. C ++, kaynak sızıntılarından kaçınmanızı sağlayan kapsamlı bir dildir (bir çöp toplayıcının veritabanı, bağlantı veya dosya tanıtıcıları için zor olduğunu düşündüğü bir şey).
Yani, bildiğim kadarıyla gördüğüm kadarıyla, bu ise buna değer.
Edit 2010-01-29: Atmayan takas hakkında
nobar, "istisna güvenli kod nasıl yazılır" ın bir parçası olduğu için oldukça alakalı olduğuna inandığım bir yorum yaptı:
- [ben] Takas asla başarısız olmaz (fırlatma takas bile yazma)
- [nobar] Bu, özel olarak yazılmış
swap()
işlevler için iyi bir öneridir . Bununla birlikte, std::swap()
dahili olarak kullandığı işlemlere dayanarak başarısız olabileceğine dikkat edilmelidir.
varsayılan std::swap
, bazı nesneler için atabileceğiniz kopyalar ve atamalar yapar. Böylece, varsayılan takas sınıflarınız için veya hatta STL sınıfları için kullanılabilir. Bildiğim kadarıyla ++ standardı ilgilidir C, takas işlemi için vector
, deque
ve list
o olabilir için ise atmaz map
karşılaştırma funktoru (Bkz kopya inşaat atabilir eğer C ++ Dili, Special Edition, apandis E E.4.3 Programlama Değiştir ).
Vektörün takasının Visual C ++ 2008 uygulamasına bakıldığında, iki vektörün aynı ayırıcıya (yani, normal durumda) sahip olması durumunda vektörün takası atmaz, ancak farklı ayırıcıları varsa kopyalar oluşturur. Ve böylece, bu son davaya atılabileceğini varsayıyorum.
Bu nedenle, orijinal metin hala geçerlidir: Asla bir atma takas yazma, ancak nobar'ın yorumu hatırlanmalıdır: Takas ettiğiniz nesnelerin atmayan bir takas olduğundan emin olun.
Edit 2011-11-06: İlginç makale
Bize temel / güçlü / huzursuz garantileri veren Dave Abrahams , bir makalede STL istisnasını güvence altına alma konusundaki deneyimini şöyle anlattı:
http://www.boost.org/community/exception_safety.html
Her durumun test edildiğinden emin olmak için otomatik birim testine dayandığı 7. noktaya (istisna güvenliği için otomatik test) bakın. Sanırım bu bölüm, yazarın "Bundan emin olabilir misiniz, " sorusuna mükemmel bir cevap .
Düzenleme 2013/05/31: dan Açıklama dionadar
t.integer += 1;
taşma olmayacağının garantisi istisna güvenli DEĞİLDİR ve aslında teknik olarak UB'yi çağırabilir! (İmzalı taşma UB: C ++ 11 5/4 "Bir ifadenin değerlendirilmesi sırasında sonuç matematiksel olarak tanımlanmazsa veya türü için temsil edilebilir değerler aralığında değilse, davranış tanımsızdır.") tamsayı taşmaz, ancak hesaplamalarını bir eşdeğerlik sınıfı modulo 2 ^ # bit ile yapın.
Dionadar, aslında tanımlanmamış davranışa sahip aşağıdaki satıra atıfta bulunuyor.
t.integer += 1 ; // 1. nothrow/nofail
Buradaki çözüm std::numeric_limits<T>::max()
, eklemeyi yapmadan önce tamsayı zaten maksimum değerinde (kullanarak ) olup olmadığını doğrulamaktır .
Benim hatam "Normal hata vs hata" bölümünde, yani bir hata gider. Gerekçeyi geçersiz kılmaz ve istisna-güvenli kodun işe yaramaz olduğu anlamına gelmez, çünkü elde edilmesi imkansızdır. Kendinizi bilgisayarın kapanmasına, derleyici hatalarına, hatta hatalarınıza veya diğer hatalara karşı koruyamazsınız. Mükemmelliğe ulaşamazsınız, ancak mümkün olduğunca yaklaşmaya çalışabilirsiniz.
Kodu Dionadar'ın yorumları göz önünde bulundurarak düzelttim.