ऑपरेटर, एक्सप्रेशन और कन्वर्शन
ऑपरेटर प्रेसिडेंस, अरिथमेटिक कन्वर्शन, साइन्ड/अनसाइन्ड मिलाने के खतरे, और C++ के चार नामित कास्ट में महारत हासिल करें।
एक्सप्रेशन: गणना के बिल्डिंग ब्लॉक
C++ में एक्सप्रेशन (expression) ऑपरेटरों और ऑपरेंड का एक क्रम है जो कोई वैल्यू निकालता है। x = a + b; जैसे सरल स्टेटमेंट में भी कई उप-एक्सप्रेशन होते हैं: a, b, a + b, और x = a + b (असाइनमेंट खुद एक एक्सप्रेशन है जो असाइन की गई वैल्यू लौटाता है)।
हर एक्सप्रेशन के दो गुण होते हैं: एक टाइप और एक वैल्यू कैटेगरी (lvalue, rvalue आदि)। इन गुणों को समझना ज़रूरी है, क्योंकि C++ टाइपों के बीच इम्प्लिसिट कन्वर्शन लागू करती है, और अगर आप सावधान न हों तो इन कन्वर्शन के नियम चौंकाने वाले नतीजे दे सकते हैं।
ऑपरेटर प्रेसिडेंस (precedence) तय करता है कि ऑपरेटर कैसे समूहित होते हैं (कौन ज़्यादा कसकर बँधता है), जबकि एसोसिएटिविटी (associativity) समान प्रेसिडेंस स्तर वाले ऑपरेटरों के समूहन की दिशा तय करती है। इन्हें गलत समझने से ऐसे बारीक बग बनते हैं जो बिना किसी वार्निंग के कंपाइल हो जाते हैं।
ऑपरेटर प्रेसिडेंस की चौंकाने वाली बातें
ऑपरेटर प्रेसिडेंस बगों के सबसे आम स्रोतों में से एक है। शक हो तो अपना आशय स्पष्ट करने के लिए कोष्ठक (parentheses) का उपयोग करें:
#include <iostream>
int main() {
// Precedence trap #1: bitwise vs comparison
int x = 5;
// This does NOT check if bit 1 is set:
if (x & 1 == 0) { // Parsed as: x & (1 == 0) → x & 0 → 0
std::cout << "Never reached!\n";
}
// Correct:
if ((x & 1) == 0) { // Parentheses fix the grouping
std::cout << "Even\n";
}
// Precedence trap #2: ternary vs assignment
int a = 1, b = 2;
int result = a > b ? a : b = 10; // Parsed as: (a > b ? a : b) = 10 — ERROR!
// Correct:
int correct = (a > b) ? a : b; // Parenthesize for clarity
// Precedence trap #3: pointer dereference vs member access
// (*ptr).member vs ptr->member — both are equivalent
// *ptr.member is parsed as *(ptr.member) — usually a compile error
// Short-circuit evaluation
int* ptr = nullptr;
// Safe: the second operand is NOT evaluated if the first is false
if (ptr != nullptr && *ptr > 0) {
std::cout << "Value: " << *ptr << '\n';
}
// The && and || operators guarantee left-to-right evaluation and short-circuit
// Overloaded && and || do NOT short-circuit — avoid overloading them
}इस उदाहरण को कंपाइल करके चलाने के लिए चलाएं दबाएं।
अरिथमेटिक कन्वर्शन और इंटीजर प्रमोशन
जब आप किसी एक्सप्रेशन में अलग-अलग टाइप मिलाते हैं, तो C++ usual arithmetic conversions नामक नियमों के अनुसार इम्प्लिसिट कन्वर्शन लागू करती है:
1. इंटीजर प्रमोशन (Integer promotion): int से छोटे टाइप (char, short, bool) को पहले int में प्रमोट किया जाता है (या unsigned int में, अगर वैल्यू int में न समा सके)। इसका मतलब है कि char + char का नतीजा int होता है, char नहीं।
2. संतुलन (Balancing): अगर प्रमोशन के बाद भी ऑपरेंड के टाइप अलग रहें, तो "छोटे" टाइप को "बड़े" टाइप में बदला जाता है, इस पदानुक्रम के अनुसार: int → unsigned int → long → unsigned long → long long → unsigned long long → float → double → long double।
3. Signed/अनसाइन्ड की पारस्परिक क्रिया: जब कोई साइन्ड टाइप उसी रैंक के किसी अनसाइन्ड टाइप से मिलता है, तो साइन्ड वैल्यू को अनसाइन्ड में बदल दिया जाता है। सबसे खतरनाक इम्प्लिसिट कन्वर्शन यहीं होते हैं।
साइन्ड और अनसाइन्ड इंटीजर मिलाना C++ के सबसे आम और खतरनाक जालों में से एक है। जब किसी ऋणात्मक साइन्ड वैल्यू को इम्प्लिसिटली अनसाइन्ड में बदला जाता है, तो वह घूमकर एक बहुत बड़ी धनात्मक संख्या बन जाती है (क्योंकि अनसाइन्ड टाइप मॉड्यूलर अरिथमेटिक का उपयोग करते हैं)।
यह अक्सर तब होता है जब int वैल्यू की तुलना size_t (जो unsigned है) से की जाती है — उदाहरण के लिए, std::vector पर int लूप काउंटर से इटरेट करना और उसकी तुलना vector.size() से करना।
इन समस्याओं को पकड़ने के लिए -Wsign-compare (जो -Wall में शामिल है) के साथ कंपाइल करें।
- अनसाइन्ड में बदला गया ऋणात्मक int एक बहुत बड़ी संख्या बन जाता है (जैसे 32-बिट में -1 बदलकर 4294967295 हो जाता है)
- साइन्ड और अनसाइन्ड वैल्यू की तुलना करने पर साइन्ड वैल्यू पहले अनसाइन्ड में बदल दी जाती है
- vector.size() size_t (unsigned) लौटाता है — ऋणात्मक int से तुलना करने पर गलत नतीजे मिलते हैं
- साइन्ड/अनसाइन्ड तुलना की समस्याओं को कंपाइल टाइम पर पकड़ने के लिए -Wsign-compare का उपयोग करें
C++ नामित कास्ट बनाम C-स्टाइल कास्ट
C++ चार नामित कास्ट ऑपरेटर देती है जो खतरनाक C-स्टाइल कास्ट (Type)expr की जगह लेते हैं। हर एक का एक विशिष्ट उद्देश्य है और सुरक्षा के अलग-अलग स्तर हैं:
#include <iostream>
#include <cstdint>
class Base {
public:
virtual ~Base() = default;
virtual void speak() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void speak() override { std::cout << "Derived\n"; }
void only_in_derived() { std::cout << "Special!\n"; }
};
int main() {
// static_cast — compile-time checked, for "natural" conversions
double pi = 3.14159;
int truncated = static_cast<int>(pi); // Explicit: you acknowledge the truncation
std::cout << truncated << '\n'; // 3
// dynamic_cast — runtime-checked, for safe downcasting in class hierarchies
Base* base_ptr = new Derived();
Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr);
if (derived_ptr) { // Check for success!
derived_ptr->only_in_derived(); // Safe
}
// const_cast — adds or removes const (use sparingly!)
const int ci = 42;
// int& mutable_ref = const_cast<int&>(ci); // Legal but modifying ci is UB!
// Only valid use: calling a non-const API with a const object you know won't be modified
// reinterpret_cast — reinterprets the bit pattern (dangerous!)
std::int64_t address = reinterpret_cast<std::int64_t>(base_ptr);
std::cout << "Address: " << address << '\n';
// C-style cast: (int)pi — NEVER use this in C++
// It tries static_cast, then const_cast, then reinterpret_cast —
// you don't know which one was applied!
delete base_ptr;
}इस उदाहरण को कंपाइल करके चलाने के लिए चलाएं दबाएं।
आशय स्पष्ट करने और कंपाइलर से उचित जाँच पाने के लिए नामित कास्ट का उपयोग करें:
- static_cast — सुपरिभाषित कन्वर्शन के लिए: संख्यात्मक टाइप कन्वर्शन, क्लास हाइरार्की में अपकास्ट, और void* से टाइप्ड पॉइंटर। यह वह कास्ट है जिसका आप सबसे ज़्यादा उपयोग करेंगे।
- dynamic_cast — पॉलीमॉर्फ़िक हाइरार्की में सुरक्षित डाउनकास्टिंग के लिए। बेस क्लास में कम से कम एक वर्चुअल फ़ंक्शन होना ज़रूरी है। विफल होने पर nullptr लौटाता है (पॉइंटर के लिए) या std::bad_cast थ्रो करता है (रेफ़रेंस के लिए)। इसकी रनटाइम लागत होती है।
- const_cast — केवल उन पुराने (legacy) API से इंटरफ़ेस करने के लिए जो const-correct नहीं हैं। किसी वास्तव में const ऑब्जेक्ट को बदलने के लिए इसका कभी उपयोग न करें।
- reinterpret_cast — निम्न-स्तरीय बिट पुनर्व्याख्या के लिए। यह लगभग हमेशा प्लेटफ़ॉर्म-विशिष्ट कोड का संकेत देता है। एप्लिकेशन कोड में इसकी शायद ही कभी ज़रूरत पड़ती है।
C++ में C-स्टाइल कास्ट (Type)expr का कभी उपयोग न करें — वे छिपा देते हैं कि कौन-सा कन्वर्शन हो रहा है और सुरक्षा जाँचों को दरकिनार कर देते हैं।
- संख्यात्मक कन्वर्शन और सुरक्षित अपकास्ट के लिए static_cast का उपयोग करें — इसकी जाँच कंपाइल टाइम पर होती है
- पॉलीमॉर्फ़िक हाइरार्की में सुरक्षित डाउनकास्ट के लिए dynamic_cast का उपयोग करें — इसकी जाँच रनटाइम पर होती है
- const_cast का उपयोग केवल उन पुराने API से इंटरफ़ेस करने के लिए करें जो const-correct नहीं हैं
- C++ में C-स्टाइल कास्ट का कभी उपयोग न करें — वे छिपा देते हैं कि कौन-सा कन्वर्शन लागू हो रहा है
- ऑपरेटर प्रेसिडेंस समूहन तय करता है — शक हो तो आशय स्पष्ट करने के लिए कोष्ठक का उपयोग करें
- && और || में शॉर्ट-सर्किट इवैल्यूएशन बाएँ से दाएँ क्रम की गारंटी देता है और गैर-ज़रूरी मूल्यांकन छोड़ देता है
- इंटीजर प्रमोशन अरिथमेटिक से पहले छोटे टाइप (char, short, bool) को int में बदल देता है
- साइन्ड और अनसाइन्ड टाइप मिलाने पर साइन्ड वैल्यू अनसाइन्ड में बदल जाती है — ऋणात्मक int एक विशाल धनात्मक संख्या बन जाता है
- C-स्टाइल कास्ट के बजाय चार नामित कास्ट (static_cast, dynamic_cast, const_cast, reinterpret_cast) का उपयोग करें
- C-स्टाइल कास्ट खतरनाक हैं क्योंकि वे चुपचाप कई कास्ट टाइप आज़माते हैं और आपको बताते नहीं कि कौन-सा लागू हुआ
क्विज़ — अपना ज्ञान परखें
(15 XP)1. एक्सप्रेशन `x & 1 == 0` वास्तव में किस रूप में इवैल्यूएट होता है?
2. `-1 < 0u` (जहाँ `0u` unsigned है) की तुलना करने पर क्या होता है?
3. पॉलीमॉर्फ़िक क्लास हाइरार्की में सुरक्षित डाउनकास्टिंग के लिए आपको कौन-सा C++ कास्ट उपयोग करना चाहिए?