- अगर
floatका दशमलव हिस्सा हटाने के बाद मान target integer type की range से बाहर हो, तो undefined behavior (UB) होता है, और implicit conversion, function-style cast, तथाstatic_cast— सभी प्रभावित होते हैं -Wallऔर-Wextraइसके लिए चेतावनी नहीं देते, और-Wconversionभी सिर्फ implicit conversion को detect करता है, इसलिए यह आसानी से छूट सकता है- Microsoft GSL का सुरक्षित narrowing conversion फ़ंक्शन
gsl::narrowभी कुछ floating-point → integer input पर UB पैदा करता है, इसलिए वह documentation में बताए गए “represent न हो सकने वाले मान पर exception फेंकने” वाले व्यवहार को निभा नहीं पाता - x86 का
CVTTSS2SIrepresent न हो सकने वाले मान कोINT_MINकी तरह handle करता है, लेकिन AArch64 काFCVTZSsaturated conversion करता है और NaN को 0 में बदल देता है, इसलिए hardware के अनुसार परिणाम अलग हो सकते हैं - सुरक्षित रूपांतरण के लिए cast से पहले range check करना चाहिए, और Clang·GCC के UBSan विकल्प
-fsanitize=float-cast-overflowसे इस समस्या को detect किया जा सकता है
रूपांतरण नियम और detection की सीमाएँ
- C++ floating-point-से-integer conversion rules के अनुसार, दशमलव हिस्सा हटाने के बाद अगर मान target integer type में फिट नहीं होता, तो undefined behavior होता है
- Target
unsignedहो तब भी modular arithmetic लागू नहीं होती int i0 = f,int(f),static_cast<int>(f)— तीनों कुछ input पर UB पैदा कर सकते हैं
- Target
- सिर्फ सामान्य compiler warnings से इस समस्या को पूरी तरह पकड़ना मुश्किल है
-Wallऔर-Wextraतीनों तरह के conversion पर warning नहीं देते-Wconversionसिर्फ implicit conversion पर warning देता है
- भले ही मौजूदा processor और compiler पर program चलता रहे, परिणाम platform के अनुसार बदल सकते हैं
- x86 का
CVTTSS2SIrepresent न हो सकने वाले input कोINT_MINपर map करता है - AArch64 का
FCVTZSsaturation करता है और NaN को 0 पर map करता है - एक बार UB execute हो जाने पर, compiler किसी दूसरे conversion को लागू करते समय code के अचानक गलत काम करने का कारण बन सकता है
- x86 का
GSL का मामला और सुरक्षित उपाय
- Microsoft Guidelines Support Library का
gsl::narrowएक सुरक्षित narrowing conversion के रूप में पेश किया जाता है, जो target type में represent न हो सकने वाले मान पर exception फेंकता है- लेकिन वास्तविक floating-point → integer conversion कुछ input पर पहले ही UB execute कर देता है, इसलिए यह documentation से मेल नहीं खाता
- GSL पक्ष का मानना था कि target platform पर hardware trap representation को नहीं छुआ जाता, इसलिए अंदरूनी UB हानिरहित है; यही तर्क code में भी दिखता है और समस्या सुधारी नहीं गई है
- सही समाधान है cast से पहले range check करना
- Rust के saturated conversion तरीके पर आधारित cpp-clamp-cast proof-of-concept library उपलब्ध है
- Clang और GCC के Undefined Behavior Sanitizer में
-fsanitize=float-cast-overflowका उपयोग करके इस UB को detect किया जा सकता है- सभी C++ code को UBSan के साथ test करने की सिफारिश की जाती है
1 टिप्पणियां
Lobste.rs की राय
C और C++ के सूक्ष्म undefined behavior के कई मामलों के बारे में पता था, फिर भी यह मामला चौंकाने वाला था
Rust ने भी LLVM IR के वही float→int conversion नियम विरासत में लिए थे, इसलिए कुछ समय तक वहाँ भी undefined behavior था, और 2020 में जाकर इसे ठीक किया गया ताकि अधिक जटिल IR बनाया जाए। performance में बड़ा फर्क होने के कारण, high-performance loops के लिए जहाँ यह पता हो कि value finite है और target type की range में है, unchecked float→int conversion भी दिया गया है
C++ Core Guidelines Library का इसे हल्के में लेना बेतुका है। LLVM ने conversion से मिली जानकारी का उपयोग करके bounds checks हटा दिए, और उसके परिणामस्वरूप ऐसा मामला हुआ जिसमें bounds check होने के बावजूद array bounds से बाहर चला गया। अगर memory safety और undefined behavior से बचने को गंभीरता से लिया जाए, तो इसे अनदेखा नहीं किया जा सकता, और Herb Sutter का “benign undefined behavior” कहना भी निराशाजनक है
पता था कि C++ में undefined behavior बहुत है, लेकिन यह खास तौर पर चौंकाता है। सोच रहा हूँ कि क्या conversion को जितना संभव हो उतना तेज़ बनाने की कोशिश में, और क्योंकि अलग-अलग architectures पर इस extreme case को अलग तरह से संभालने वाले instructions हैं, इसे undefined behavior छोड़ा गया। signed integer overflow की तरह यही इस नियम का मुख्य कारण लगता है
undefined behavior को use-after-free जैसी चीज़ों तक सीमित होना चाहिए, जहाँ C abstract machine के बाहर के effects के कारण same platform पर भी consistent result की गारंटी नहीं दी जा सकती, या कुछ targets पर trap हो सकता है
standard-compliant implementation अपने स्तर पर undefined behavior को define कर सकती है, और GCC कुछ मदों में ऐसा करता भी है। अगर बिना performance cost के stable semantics की गारंटी दी जा सकती है, तो target-specific float→int instruction के behavior को ही define किया जा सकता है, लेकिन दूसरी implementations में यह फिर भी undefined behavior रहेगा
fctiwseries saturating conversion करती है, NaN कोINT_MINमें बदलती है और FPSCR flags भी set करती है। 64-bit Power ISA कीfctidभी बड़े integers के लिए यही करती है, लेकिन इनमें से कोई भी AArch64 या x86 के behavior से मेल नहीं खाताऐसे मामलों में इसे implementation-defined behavior या unspecified value की तरह संभालना ज़्यादा सही है। सिर्फ infinity को
intमें convert करने की वजह से पूरा program C++ standard के नियंत्रण से बाहर चला जाए, यह समझ से परे हैC++26 ने कुछ अव्यावहारिक undefined behavior हटा दिए हैं, और यह भी स्पष्ट रूप से हटाए जाने योग्य उम्मीदवार है। प्रयोग से लगता है कि GCC और Clang इस undefined behavior का optimization में उपयोग नहीं करते, इसलिए वास्तविक असर सीमित दिखता है
यह एक और अप्रिय उदाहरण है कि specification के पास इस operation को undefined behavior घोषित करने का कोई कारण नहीं है
IEEE 754 को नापसंद करने की एक और वजह मिल गई। अगर दूसरी libraries या performance के कारण मजबूरी न हो, तो floating-point की जगह pure integers, बड़े integer numerator-denominator वाले rationals, और fixed-point decimals का जितना हो सके उतना उपयोग करता हूँ
अगर C या C++ इस्तेमाल किया है, तो
float32औरint32, यहाँ तक किint64, के बीच का अंतर चौंकाने वाला नहीं होना चाहिए। बड़े exponent वालाfloat32,int64से कहीं बड़े integer values दिखा सकता हैजब ऐसे types हों जो एक-दूसरे के superset नहीं हैं और जिनकी representational capability अलग है, तो language syntax से अलग भी float→int conversion को safe मानने का कोई कारण नहीं है
uint32_tभीuint64_tके सभी values represent नहीं कर सकता, लेकिन conversion semantics truncation के रूप में define हैं। यहाँ मूल समस्या यह है कि कुछ input पर compiler कुछ भी कर सकता हैlow-level language होने पर
CVTTSS2SIयाFCVTZSजैसे assembly instructions से इसे map करना भी संभव है। low-level language का assembly से निकट होना, और दूसरे “benign” undefined behavior पर भी बहुत विवाद होना, इस तथ्य को और चौंकाने वाला बनाता है कि यह conversion undefined behavior की अनुमति देता हैलेकिन unrelated code का अजीब तरह से miscompile हो जाना, hard drive को format कर देना, या “नाक से दानव निकल आना” तक अनुमति होना चौंकाता है
double-free या array bounds से बाहर write में कुछ भी हो सकता है—इस तरह का undefined behavior समझ में आता है, लेकिन C और C++ वहाँ भी इसका अति-उपयोग करते हैं जहाँ implementation-defined garbage value या program abort जैसे अधिक सख्त नियम रखे जा सकते थे। Rust कुछ integer overflow को debug mode में trap करता है और release mode में unspecified value लौटाता है, लेकिन उसे बे-सिर-पैर के दूसरे code को तोड़ने नहीं देता
C++ को Java या Rust बनने की ज़रूरत नहीं है, लेकिन अनावश्यक undefined behavior को कम करने से यह निश्चित रूप से बेहतर भाषा बनेगी