Tarih: C++20'den önce, C++ standardı imzalı tam sayılar için üç farklı temsil biçimine izin vermekteydi: işaret-magnitüd, birin tamamı ve iki'nin tamamı. Bu mimari tarafsızlık, standardın negatif imzalı tam sayılar için sağ kaydırmayı uygulanma tanımlı olarak kabul etmesine neden oldu ve işlemin aritmetik kaydırma (işaret bitini koruma) mi yoksa mantıksal kaydırma (sıfır doldurma) mı yapacağı konusunda taşınabilir garantiler vermedi. Düşük seviyeli sistemlerin geliştiricileri, donanım platformlarında tutarlı bit çıkarım davranışını sağlamak için genellikle imzasız türe dönüştürme yapmak ya da standart dışı derleyici eklentilerine güvenmek zorunda kaldılar.
Problem: Belirlenmiş bir temsilin eksikliği, ağ protokolü ayrıştırma, gömülü işaret işleme ve tam nokta aritmetiği gibi sistem programlama görevleri için bir taşınabilirlik tehlikesi oluşturuyordu. Negatif miktarlar üzerinde etkili bölme işlemi için aritmetik sağ kaydırmaya (örneğin, -5 >> 1 sonucu -3) güvenen kod, işaret-magnitüd veya birin tamamı temsilleri kullanan mimarilerde yanlış sonuçlar üretiyor, bu da teşhis edilmesi zor olan ince veri bozulmaları veya kontrol akışı hatalarına yol açıyordu.
Çözüm: C++20, iki'nin tamamını imzalı tam sayılar için tek izin verilen temsil olarak standartlaştırmaktadır. Bu standardizasyon, negatif bir imzalı tam sayının sağ kaydırılmasının aritmetik bir kaydırma gerçekleştirmesini garanti eder, bu matematiksel olarak aşağıya doğru bölmeyi (negatif sonsuzluğa doğru yuvarlama) eşdeğerdir. Bu nedenle, E1 >> E2 ifadesi artık negatif E_1 olduğunda bile güvenilir bir şekilde $\lfloor E_1 / 2^{E_2}
floor$ sonucunu verir. Ancak bu garanti, yalnızca bit düzeyindeki işlem için geçerlidir; bu durum, sonuçları sıfıra doğru kesen ve tanımsız davranışı soldan kaydırmalardan veya taşma senaryolarından kurtarmayan tam sayı bölme operatörü / ile farklıdır.
#include <iostream> int main() { int neg = -5; // C++20 aritmetik kaydırmayı garanti eder: -5 / 2^1 aşağıya yuvarlanmış = -3 int shifted = neg >> 1; // Tam sayı bölme sıfıra doğru keser: -5 / 2 = -2 int divided = neg / 2; std::cout << "Kaydırılmış: " << shifted << " (aşağıya yuvarlama) "; std::cout << "Bölünmüş: " << divided << " (sıfıra doğru kes) "; }
Ayrıntılı örnek: Bir geliştirme ekibi, endüstriyel sensörler için yüksek hassasiyetli sıcaklık okumalarını 32 bit imzalı tamsayılar olarak kodlamak için sabit nokta aritmetiği kullanan bir çapraz platform telemetri kütüphanesini sürdürüyordu. Kaynak sınırlı mikro kontrolörlerde performansı en üst düzeye çıkarmak için, yazılım gereksiz kayan nokta bölmesini yaklaşmak için bit düzeyindeki sağ kaydırmaları kullanarak brüt ADC değerlerini mühendislik birimlerine ölçeklendirdi. Kütüphaneyi geriye dönük test için kullanılan eski bir ana çerçeve simülatörü ile doğrulamak amacıyla bir geçiş çabası sırasında, ekibin negatif sıcaklık okumalarının (sıfır altı koşulları temsil eden) tek bit ile yanlış hesaplandığını keşfetmesi, simüle edilmiş güvenlik kesme tetikleyicilerinin başarısız olmasına neden oldu.
Problem tanımı: Eski simülatörün derleyicisi, imzalı tam sayılar için birin tamamı temsili kullanıyordu; burada negatif bir değerin sağ kaydırması beklenildiği gibi işaret bitini yaymıyordu. Bu tutarsızlık, sabit nokta ölçeklendirme mantığının negatif değerleri sıfıra doğru yuvarlamasına neden olarak, birçok sensör füzyon hesaplaması boyunca bir LSB (En Az Anlamlı Bit) sistematik bir kayma oluşturarak güvenlik tolerans eşiklerini aşmasına neden oluyordu.
Çözüm 1: Savunmacı imzasız dönüştürme.
Ekip, her bir sağ kaydırma işlemini imzalı tamsayıyı uint32_t'ye dönüştürerek yeniden yazmayı, kaydırmayı gerçekleştirmeyi ve ardından bitmasking ve koşullu mantık kullanarak işareti manuel olarak yeniden inşa etmeyi düşündü. Bu, host mimarisi ne olursa olsun iyi tanımlanmış imzasız anlamları zorunlu hale getirse de, kod tabanını ayrıntılı bit ile oynama makroları ile şişiriyor, matematiksel formüllerin okunabilirliğini azaltıyor ve manuel işaret yeniden inşa aşamasında hata yapma riskini artırıyordu.
Çözüm 2: Ön işleyici soyutlama katmanı. Hedefleri ne olursa olsun optimal performansı korurken, sabit nokta aritmetiği için aritmetik yeniden inşa kullanarak ve standart platformlar için yerel kaydırmalar kullanan derleyici tespiti başlığı uygulamayı değerlendirdiler. Bu yaklaşım, kaynak kodunu koşullu derleme bloklarıyla parçalamakla birlikte, derleyiciye özgü inceliklerin kapsamlı bir veritabanını sürdürmeyi gerektiriyor ve CI hattını karmaşık hale getirerek eski simülatör için ayrı derleme yapılandırmaları gerektiriyordu.
Çözüm 3: Araç zinciri modernizasyon mandası. Ekip, simülatör ortamını C++20 uyumlu bir araç zincirine yükseltmeye ve birin tamamı eski desteğini emekliye ayırmaya karar verdi. Bu, orijinal, temiz kaydırma tabanlı aritmetiği korumalarına izin verdi ve tüm hedeflerin negatif sağ kaydırmaları aşağıya doğru bölme şeklinde yorumlayacaklarını garanti etti, savunmacı kodlama desenleri veya platforma özel dallanma gereksinimini ortadan kaldırdı.
Hangi çözüm seçildi (ve neden): Çözüm 3, test altyapısını modernize etmenin mühendislik maliyetinin eski bir tam sayı temsilini desteklemenin sürekli bakım yükünden önemli ölçüde daha düşük olduğu için seçildi. C++20 iki'nin tamamı garantisi, geliştirme çalışma istasyonu, CI sunucuları ve üretim mikro kontrolörleri arasında aynı bit düzeyi anlamsallığını sağlamak için bir standart destekli sözleşme sağladı.
Sonuç: Telemetri kütüphanesi, güncellenmiş araç zincirinde değişiklik yapmadan derlendi ve güvenlik kritik birim testleri ilk yürütmede başarılı oldu. Ekip, yaklaşık 150 satır savunmacı dönüştürme makrosunu ve koşullu derleme bloklarını kaldırdı. Nihai yazılım, hem yeni simülatörde hem de fiziksel donanımda ISO kalibreli doğruluk sağladı ve donanım spesifik yamanın gerekmediği düzenleyici validationsözgruplarını geçerek başarılı oldu.
Soru: C++20'nin iki'nin tamamı temsil garantisi, negatif imzalı bir tam sayının sağ kaydırılmasının, o tam sayıyı ilgili iki kuvvetine bölme işleminden matematiksel olarak farklı bir sonuç vermesini neden ima eder?
Cevap: C++20'de, negatif bir imzalı tam sayının sağ kaydırması aritmetik bir kaydırma gerçekleştirir, bu da aşağıya doğru bölmeyi (negatif sonsuzluğa yuvarlama) uygular. Aksine, tam sayı bölme operatörü / sonucu sıfıra doğru keser. Örneğin, -5 >> 1 ifadesi -3 değerini verirken, -5 / 2 ifadesi -2 değerini verir. Adaylar genellikle bu işlemlerin değiştirilebilir optimizasyonlar olduğuna varsayımıyla yaklaşsalar da, bu eşitlik yalnızca negatif olmayan operatörler için geçerlidir. Bu ayrım, sabit nokta aritmetiği veya yuvarlama algoritmalarını uygularken, yuvarlama yönü hesaplamanın sayısal kararlılığını etkilediği için önemlidir.
Soru: C++20 iki'nin tamamı zorunluluğu (-1) << 1 ifadesini belirlenmiş hale getirir mi?
Cevap: Hayır, negatif bir imzalı tam sayının sola kaydırılması hala tanımsız bir davranıştır. C++20 standardı, operandın negatif olduğu, kaydırma miktarının türünün bit genişliğine eşit veya daha büyük olduğu veya sonucun işaret bitine taşma yaşadığı durumlarda sola kaydırmayı yasaklamaya devam etmektedir. İki'nin tamamı altında bit desenini düzeltirken, standart, işaret bitine geçişin anlamlı sonucunu tanımlamaz veya taşmalara izin vermez. Tanımlı bit manipülasyonu gerektiren geliştiricilerin, taşınabilir, iki'nin kuvvetine göre modüler anlamlar elde etmek için hala imzasız bir türe dönüştürmesi gerekmektedir.
Soru: C++20 iki'nin tamamı zorunluluğu, std::abs(std::numeric_limits<int>::min()) sonucunu nasıl etkiler?
Cevap: C++20, std::numeric_limits<int>::min() değerinin $-2^{31}$ (32 bit tamsayılar için) bit deseni 100...0 ile eşit olduğunu garanti eder. Ancak, imzalı bir tam sayının pozitif aralığı yalnızca $2^{31}-1$ kadar uzanmaktadır. Sonuç olarak, minimum tamsayının mutlak değeri pozitif bir int olarak temsil edilemez ve std::abs ifadesinin INT_MIN üzerinde çağrılması, imzalı tam sayı taşması nedeniyle tanımsız bir davranış sergiler. İki'nin tamamı zorunluluğu bit temsilini netleştirirken, imzalı tam sayı aralığının asimetrik doğasını değiştirmez; bu, savunmacı sınır kontrolleri veya büyüklük karşılaştırmaları yazarken sıklıkla gözden kaçan bir ayrıntıdır.