मूव सिमेंटिक्स और std::move
समझें कि मूव सिमेंटिक्स किस समस्या को हल करता है, std::move असल में एक कास्ट है, मूव नहीं, मूव कंस्ट्रक्टर और मूव असाइनमेंट कैसे लागू करें, और कंटेनर की परफ़ॉर्मेंस के लिए noexcept क्यों ज़रूरी है।
मूव किस समस्या को हल करता है
C++11 से पहले, किसी फ़ंक्शन से बड़ा ऑब्जेक्ट रिटर्न करने के लिए उसे कॉपी करना पड़ता था। 10 लाख एलिमेंट वाले std::vector को कॉपी करने का मतलब है नई मेमोरी एलोकेट करना और हर एलिमेंट को कॉपी करना — O(n) काम, जो पूरी तरह बेकार है अगर सोर्स नष्ट होने ही वाला हो।
मूव सिमेंटिक्स (move semantics) इसे हल करता है: कॉपी करने के बजाय हम सोर्स के आंतरिक रिसोर्स (उसका पॉइंटर, साइज़, कैपेसिटी) ले लेते (steal) हैं और सोर्स को वैध लेकिन खाली अवस्था में छोड़ देते हैं। वेक्टर को मूव करना O(1) है — बस तीन पॉइंटर/इंटीजर की अदला-बदली। सोर्स नष्ट नहीं होता; वह एक खाली वेक्टर बन जाता है।
मुख्य बात: अगर कोई ऑब्जेक्ट अस्थायी (temporary) है या उसे "अब मुझे इसकी ज़रूरत नहीं" (std::move के ज़रिए) के रूप में चिह्नित किया गया है, तो उसके रिसोर्स लेना सुरक्षित है।
std::move एक कास्ट है, मूव नहीं
std::move(x) कुछ भी मूव नहीं करता। यह rvalue रेफ़रेंस में एक बिना शर्त (unconditional) कास्ट है (static_cast)। यह कंपाइलर से कहता है: "मेरा इस ऑब्जेक्ट से काम पूरा हो गया है — आप इसे अस्थायी मान सकते हैं।"
वास्तविक मूव तब होता है जब इस परिणामी rvalue रेफ़रेंस के साथ मूव कंस्ट्रक्टर या मूव असाइनमेंट ऑपरेटर कॉल होता है। अगर टाइप में कोई मूव ऑपरेशन नहीं है (जैसे const ऑब्जेक्ट), तो std::move चुपचाप कॉपी पर लौट आता है।
यह बग का एक आम स्रोत है: const std::string s = "hello"; auto s2 = std::move(s); में s कॉपी होता है, क्योंकि std::move(s) से const std::string&& बनता है, जो कॉपी कंस्ट्रक्टर के const std::string& पैरामीटर से बंध जाता है।
मूव कंस्ट्रक्टर और मूव असाइनमेंट लागू करना
मूव कंस्ट्रक्टर सोर्स से रिसोर्स ले लेता है। मूव असाइनमेंट ऑपरेटर पहले अपने रिसोर्स मुक्त करता है, फिर सोर्स से लेता है। दोनों को सोर्स को वैध और नष्ट करने योग्य (destructible) अवस्था में छोड़ना ज़रूरी है।
#include <iostream>
#include <cstring>
#include <utility>
#include <algorithm>
class Buffer {
char* data_;
std::size_t size_;
public:
// Constructor
explicit Buffer(std::size_t n) : data_(new char[n]()), size_(n) {
std::cout << "Alloc " << n << " bytes\n";
}
// Destructor
~Buffer() {
delete[] data_;
std::cout << "Free (" << size_ << ")\n";
}
// Copy constructor (expensive — O(n))
Buffer(const Buffer& other) : data_(new char[other.size_]), size_(other.size_) {
std::memcpy(data_, other.data_, size_);
std::cout << "Copy " << size_ << " bytes\n";
}
// Move constructor (cheap — O(1)) — MUST be noexcept
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // leave source in valid state
other.size_ = 0;
std::cout << "Move " << size_ << " bytes\n";
}
// Move assignment — MUST be noexcept
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_; // release own resources
data_ = other.data_; // steal source's resources
size_ = other.size_;
other.data_ = nullptr; // leave source valid
other.size_ = 0;
}
return *this;
}
// Copy assignment (omitted for brevity)
std::size_t size() const { return size_; }
};
int main() {
Buffer a(1'000'000); // Alloc 1000000 bytes
Buffer b = std::move(a); // Move 1000000 bytes — O(1)!
std::cout << "a.size: " << a.size() << '\n'; // 0 (moved-from)
std::cout << "b.size: " << b.size() << '\n'; // 1000000
}std::vector रीएलोकेशन के दौरान एलिमेंट को केवल तभी मूव करेगा जब मूव कंस्ट्रक्टर noexcept हो। अगर मूव कंस्ट्रक्टर एक्सेप्शन फेंक सकता है, तो वेक्टर को इसके बजाय कॉपी करना पड़ता है — वरना, अगर रीएलोकेशन के बीच में कोई मूव एक्सेप्शन फेंके, तो वेक्टर असंगत अवस्था में रह जाएगा, जिसमें कुछ एलिमेंट मूव हो चुके होंगे और कुछ नहीं।
इसका मतलब है कि अपने मूव कंस्ट्रक्टर पर noexcept न लगाने से वेक्टर की परफ़ॉर्मेंस चुपचाप प्रति एलिमेंट O(1) से O(n) तक गिर जाती है। यह C++ के सबसे आम परफ़ॉर्मेंस बग में से एक है।
// BAD — vector will COPY during reallocation
Widget(Widget&& other) { ... }
// GOOD — vector will MOVE during reallocation
Widget(Widget&& other) noexcept { ... }जाँचने के लिए हमेशा static_assert(std::is_nothrow_move_constructible_v का उपयोग करें।
मूव्ड-फ़्रॉम अवस्था
मूव के बाद, सोर्स ऑब्जेक्ट वैध लेकिन अनिर्दिष्ट (valid but unspecified) अवस्था में होता है। स्टैंडर्ड ये गारंटी देता है:
- ऑब्जेक्ट को सुरक्षित रूप से नष्ट किया जा सकता है।
- ऑब्जेक्ट को असाइन किया जा सकता है (उसे नई वैल्यू दी जा सकती है)।
- स्टैंडर्ड लाइब्रेरी टाइप के लिए, वे सभी ऑपरेशन काम करते हैं जिनकी कोई पूर्व-शर्त (precondition) नहीं है (जैसे size(), empty(), clear())।
स्टैंडर्ड यह गारंटी नहीं देता कि मूव्ड-फ़्रॉम ऑब्जेक्ट खाली होगा। व्यवहार में, std::string और std::vector मूव के बाद आमतौर पर खाली होते हैं, लेकिन यह इम्प्लीमेंटेशन की एक बारीकी (implementation detail) है।
प्रोडक्शन नियम: किसी ऑब्जेक्ट से मूव करने के बाद उसकी वैल्यू न पढ़ें — केवल उसे नष्ट करें या नई वैल्यू असाइन करें।
जब कंपाइलर अपने-आप मूव जनरेट करता है
कंपाइलर मूव ऑपरेशन केवल तभी जनरेट करता है जब क्लास में कोई यूज़र-डिक्लेयर्ड कॉपी ऑपरेशन, कोई यूज़र-डिक्लेयर्ड मूव ऑपरेशन और कोई यूज़र-डिक्लेयर्ड डिस्ट्रक्टर न हो (रूल ऑफ़ फ़ाइव/ज़ीरो)। अगर आप इनमें से कोई भी डिक्लेयर करते हैं, तो आपको सभी डिक्लेयर करने चाहिए।
#include <string>
#include <vector>
#include <iostream>
// GOOD: Rule of Zero — compiler generates everything
struct Employee {
std::string name;
std::vector<std::string> skills;
int id;
// No user-declared special members — compiler generates all 5
};
// BAD: Rule of Five violation — destructor suppresses moves
struct Resource {
int* data;
Resource() : data(new int(42)) {}
~Resource() { delete data; } // move operations NOT generated!
// This class will COPY where it could move
// Fix: declare all five special members
};
int main() {
Employee e1{"Alice", {"C++", "Rust"}, 1};
Employee e2 = std::move(e1); // compiler-generated move
std::cout << e2.name << '\n'; // "Alice"
static_assert(std::is_nothrow_move_constructible_v<Employee>,
"Employee should be nothrow-movable");
// static_assert(std::is_nothrow_move_constructible_v<Resource>,
// "FAILS — Resource has no move constructor");
}1. रूल ऑफ़ ज़ीरो का पालन करें — मेंबर के रूप में RAII टाइप (string, vector, unique_ptr) इस्तेमाल करके कंपाइलर को स्पेशल मेंबर जनरेट करने दें।
2. मूव ऑपरेशन को हमेशा noexcept चिह्नित करें — वरना std::vector उनका उपयोग नहीं करेगा।
3. const ऑब्जेक्ट पर std::move चुपचाप कॉपी करता है — कोड रिव्यू में इस पर नज़र रखें।
4. जिन ऑब्जेक्ट की आपको अब भी ज़रूरत है, उनसे मूव न करें — std::move केवल आख़िरी उपयोग पर ही इस्तेमाल करें।
5. std::move(local) रिटर्न न करें — इससे कॉपी एलिज़न (NRVO) रुक जाता है। बस लोकल को उसके नाम से ही रिटर्न करें।
6. static_assert(is_nothrow_move_constructible_v से जाँचें।
- मूव करना रिसोर्स को कॉपी करने (O(n)) के बजाय ले लेना (O(1)) है
std::moveसिर्फ़ T&& में एक कास्ट है — वास्तविक मूव मूव कंस्ट्रक्टर/असाइनमेंट में होता है- वेक्टर के उपयोग करने के लिए मूव कंस्ट्रक्टर और मूव असाइनमेंट
noexceptहोने चाहिए - मूव्ड-फ़्रॉम ऑब्जेक्ट वैध लेकिन अनिर्दिष्ट अवस्था में होते हैं — केवल नष्ट करें या दोबारा असाइन करें
- कंपाइलर मूव तभी अपने-आप जनरेट करता है जब कोई कॉपी/मूव/डिस्ट्रक्टर यूज़र-डिक्लेयर्ड न हो (रूल ऑफ़ ज़ीरो)
क्विज़ — अपना ज्ञान परखें
(20 XP)1. `std::move(x)` वास्तव में क्या करता है?
2. मूव कंस्ट्रक्टर को `noexcept` चिह्नित करना ज़रूरी क्यों है?
3. `const std::string s = "hi"; auto s2 = std::move(s);` से क्या होता है?