floatdeğerinin kesirli kısmı atıldıktan sonraki hali hedef tamsayı türünün aralığı dışındaysa tanımsız davranış (UB) oluşur; örtük dönüşüm, işlevsel cast vestatic_castbunun hepsinden etkilenir-Wallve-Wextrabunun için uyarı vermez;-Wconversionda yalnızca örtük dönüşümleri algıladığı için kolayca gözden kaçabilir- Microsoft GSL'in güvenli daraltıcı dönüşüm işlevi
gsl::narrowda bazı kayan nokta→tamsayı girdilerinde UB üretir; bu yüzden temsil edilemeyen değerlerde istisna fırlattığını söyleyen belgelenmiş davranışını koruyamaz - x86'daki
CVTTSS2SItemsil edilemeyen değerleriINT_MINolarak işlerken, AArch64'tekiFCVTZSdoygun dönüşüm yapar ve NaN'i 0'a çevirir; bu nedenle sonuçlar donanıma göre değişebilir - Güvenli dönüşüm için cast işleminden önce aralık kontrolü yapılmalıdır; Clang ve GCC'nin UBSan seçeneği
-fsanitize=float-cast-overflowile sorun tespit edilebilir
Dönüşüm kuralları ve tespitin sınırları
- C++ kayan nokta-tamsayı dönüşüm kurallarına göre, kesirli kısım atıldıktan sonra değer hedef tamsayı türüne sığmıyorsa tanımsız davranış oluşur
- Hedef unsigned olsa bile modüler aritmetik uygulanmaz
int i0 = f,int(f),static_cast<int>(f)üçü de bazı girdilerde UB üretir
- Yalnızca genel derleyici uyarılarıyla tüm sorunları bulmak zordur
-Wallve-Wextrabu üç dönüşümün hiçbiri için uyarı vermez-Wconversionyalnızca örtük dönüşümler için uyarı verir
- Program mevcut işlemci ve derleyicide çalışmaya devam etse bile sonuç platforma göre değişebilir
- x86'daki
CVTTSS2SI, temsil edilemeyen girdileriINT_MINdeğerine eşler - AArch64'teki
FCVTZSdoygun işleme yapar ve NaN'i 0'a eşler - Gerçekleşmiş bir UB, derleyici farklı dönüşümler uyguladığında kodun aniden hatalı çalışmasına neden olabilir
- x86'daki
GSL örneği ve güvenli yaklaşım
- Microsoft Guidelines Support Library'deki
gsl::narrow, hedef türde temsil edilemeyen değerlerde istisna fırlatan güvenli bir daraltıcı dönüşüm olarak sunulur- Ancak gerçek kayan nokta→tamsayı dönüşümü bazı girdilerde önce UB'yi çalıştırdığı için belgeyle uyuşmaz
- GSL tarafı, hedef platformda donanımsal trap gösterimini tetiklemediği sürece iç UB'nin zararsız olduğunu değerlendirdi; bu mantık koda yansıtıldı ve sorun düzeltilmedi
- Doğru çözüm, cast işleminden önce aralığı kontrol etmektir
- Rust'ın doygun dönüşüm yaklaşımını temel alan bir cpp-clamp-cast kavram kanıtlama kütüphanesi bulunuyor
- Clang ve GCC'nin Undefined Behavior Sanitizer'ında
-fsanitize=float-cast-overflowkullanılarak bu UB tespit edilebilir- Tüm C++ kodunu UBSan ile test etme yaklaşımı öneriliyor
1 yorum
Lobste.rs yorumları
C ve C++'taki pek çok incelikli tanımsız davranışı biliyor olsam da bu örnek şaşırtıcıydı.
Rust da LLVM IR'nin aynı kayan nokta→tamsayı dönüşüm kurallarını devraldığı için bir süre tanımsız davranışa sahipti; ancak 2020'de daha karmaşık IR üretecek şekilde düzeltildi. Performans farkı büyük olduğundan, değerin sonlu olduğunu ve hedef türün aralığına girdiğini bildiğiniz yüksek performanslı döngüler için denetimsiz kayan nokta→tamsayı dönüşümü de sunuyor.
C++ Core Guidelines Library'nin bunu önemsiz görmesi saçma. LLVM, dönüşümden elde ettiği bilgiyi sınır denetimlerini kaldırmak için kullandı ve bunun sonucunda sınır denetimi olmasına rağmen dizinin sınırları dışına çıkılan bir vaka var. Bellek güvenliğini ve tanımsız davranıştan kaçınmayı ciddiye alıyorsanız bunu görmezden gelemezsiniz; Herb Sutter'ın “iyi huylu tanımsız davranış”tan söz etmesi de hayal kırıklığı.
C++'ta çok sayıda tanımsız davranış olduğunu biliyordum ama bu özellikle şaşırtıcı. Dönüşümü olabildiğince hızlı yapmak istemiş olmaları ve mimariden mimariye bu uç değerleri farklı işleyen komutlar bulunması nedeniyle bunu tanımsız davranış olarak mı bıraktılar, merak ediyorum. İşaretli tamsayı taşmasına benzer şekilde, bu tür kuralların ortaya çıkmasının başlıca nedeni bu gibi görünüyor.
Tanımsız davranış; serbest bırakıldıktan sonra kullanma gibi C soyut makinesinin dışındaki etkiler nedeniyle aynı platformda bile tutarlı sonuç garanti edilemeyen ya da bazı hedeflerde trap'e düşebilecek durumlarla sınırlanmalı.
Standarda uygun bir uygulama tanımsız davranışı kendisi tanımlayabilir; GCC de bazı maddelerde bunu yapıyor. Kararlı semantik performans maliyeti olmadan garanti edilebiliyorsa, hedefe özgü kayan nokta→tamsayı komutunun davranışı aynen tanımlanabilir; ancak diğer uygulamalarda yine de tanımsız davranış olur.
fctiwailesi saturating conversion yapar, NaN'iINT_MINe çevirir ve FPSCR bayraklarını da ayarlar. 64 bit Power ISA'dakifctidde daha büyük tamsayılar için aynı işlemi yapar; ancak ikisi de AArch64 veya x86'nın davranışıyla uyuşmaz.Böyle durumlarda bunu uygulama tanımlı davranış ya da belirtilmemiş bir değer olarak ele almak daha doğru. Sonsuzu
inte dönüştürdü diye tüm programın C++ standardının kontrolü dışına çıkması mantıklı değil.C++26 bazı mantıksız tanımsız davranışları kaldırdı ve bu da açıkça kaldırılmaya aday. Deneylere göre GCC ve Clang bu tanımsız davranışı optimizasyonda kullanmıyor gibi, bu yüzden pratik etkisi sınırlı görünüyor.
Spesifikasyonun bu işlemi tanımsız davranış olarak tanımlaması için hiçbir neden olmayan bir başka can sıkıcı örnek.
IEEE 754'ten hoşlanmamak için bir neden daha çıktı. Başka kütüphaneler veya performans nedeniyle mutlaka gerekmedikçe kayan nokta yerine mümkün olduğunca saf tamsayı, büyük tamsayı pay ve payda kullanan rasyonel sayılar ya da sabit noktalı ondalık kullanıyorum.
C veya C++ kullandıysanız
float32ileint32, hattaint64arasındaki fark bile şaşırtıcı olmamalı. Büyük üs kullanan birfloat32,int64ten çok daha büyük tamsayı değerleri temsil edebilir.Birbirinin üst kümesi olmayan ve ifade gücü farklı biçimler arasında, dil sözdiziminden bağımsız olarak kayan nokta→tamsayı dönüşümünün güvenli olduğunu varsaymak için neden yok.
uint32_tdeuint64_tnin tüm değerlerini temsil edemez ama dönüşüm semantiği kırpma olarak tanımlanmıştır. Burada esasen farklı olan sorun, belirli girdilerde derleyicinin her şeyi yapabilmesidir.Düşük seviyeli bir dilde bunu
CVTTSS2SIveyaFCVTZSgibi assembly komutlarına karşılık gelecek şekilde yapmak da mümkün. Düşük seviyeli diller assembly'ye yakın olduğundan ve diğer “iyi huylu” tanımsız davranışlar da çok tartışmalı olduğundan, bu dönüşümün tanımsız davranışa izin vermesi daha da şaşırtıcı.Ama ilgisiz kodu tuhaf biçimde yanlış derlemeye, sabit diski biçimlendirmeye ya da “burnunuzdan iblisler çıkmasına” bile izin verilmesi şaşırtıcı.
Çift free veya dizi sınırı dışına yazma için her şeyin olabileceği tanımsız davranış kavramı makul; ancak C ve C++, uygulama tanımlı çöp değerler ya da programın durdurulması gibi daha sıkı kurallar konabilecek yerlerde bile bunu aşırı kullanıyor. Rust, bazı tamsayı taşmalarını debug modunda trap'e düşürür ve release modunda belirtilmemiş bir değer döndürür; ama alakasız kodu bozmasına izin vermez.
C++'ın Java veya Rust olması gerekmiyor, ancak gereksiz tanımsız davranışları azaltmak onu kesinlikle daha iyi bir dil yapar.