सामग्री पर जाएं

कंस्ट्रक्टर, डिस्ट्रक्टर और Rule of 0/3/5

C++ ऑब्जेक्ट का पूरा जीवनचक्र — निर्माण से लेकर विनाश तक — समझें, और जानें कि स्पेशल मेंबर फ़ंक्शन कब लिखने हैं (या नहीं लिखने हैं)।

C++ में ऑब्जेक्ट का जीवनचक्र

C++ में आपका इस पर सीधा नियंत्रण होता है कि ऑब्जेक्ट कब और कैसे बनाए और नष्ट किए जाएँ। कंपाइलर अपने आप छह स्पेशल मेंबर फ़ंक्शन तक जनरेट कर सकता है: डिफ़ॉल्ट कंस्ट्रक्टर, कॉपी कंस्ट्रक्टर, कॉपी असाइनमेंट ऑपरेटर, मूव कंस्ट्रक्टर, मूव असाइनमेंट ऑपरेटर और डिस्ट्रक्टर। यह समझना कि अपना खुद का कब लिखें — और कंपाइलर को कब करने दें — C++ के सबसे महत्वपूर्ण कौशलों में से एक है।

कंस्ट्रक्टर के प्रकार

कंस्ट्रक्टर किसी ऑब्जेक्ट को इनिशियलाइज़ करते हैं। C++ कई प्रकार के कंस्ट्रक्टर का समर्थन करता है: डिफ़ॉल्ट (बिना आर्गुमेंट), पैरामीटराइज़्ड, कन्वर्टिंग (एकल आर्गुमेंट, इम्प्लिसिट कन्वर्ज़न) और डेलिगेटिंग (एक कंस्ट्रक्टर दूसरे को कॉल करता है)।

constructors.cpp
#include <iostream>
#include <string>

class Temperature {
    double celsius_;
    std::string label_;

public:
    // Default constructor
    Temperature() : celsius_{0.0}, label_{"unknown"} {}

    // Parameterized constructor
    Temperature(double c, const std::string& label)
        : celsius_{c}, label_{label} {}

    // Converting constructor — allows implicit conversion from double
    Temperature(double c) : celsius_{c}, label_{"sensor"} {}

    // Delegating constructor — reuses another constructor
    Temperature(const std::string& label)
        : Temperature{0.0, label} {}  // delegates to parameterized

    void print() const {
        std::cout << label_ << ": " << celsius_ << "°C\n";
    }
};

int main() {
    Temperature t1;                   // default
    Temperature t2{36.6, "body"};     // parameterized
    Temperature t3 = 100.0;           // converting (implicit)
    Temperature t4{"outdoor"};        // delegating

    t1.print();  // unknown: 0°C
    t2.print();  // body: 36.6°C
    t3.print();  // sensor: 100°C
    t4.print();  // outdoor: 0°C
    return 0;
}

explicit कीवर्ड

एकल-आर्गुमेंट वाले कंस्ट्रक्टर इम्प्लिसिट कन्वर्ज़न संभव बनाते हैं, जिनसे सूक्ष्म बग हो सकते हैं। अनचाहे कन्वर्ज़न रोकने के लिए explicit इस्तेमाल करें — लगभग हमेशा आप यही चाहेंगे।

explicit.cpp
#include <iostream>
#include <string>

class UserId {
    int id_;
public:
    explicit UserId(int id) : id_{id} {}
    int get() const { return id_; }
};

void process(UserId uid) {
    std::cout << "Processing user #" << uid.get() << '\n';
}

int main() {
    UserId u1{42};       // OK: direct initialization
    // UserId u2 = 42;   // ERROR: implicit conversion blocked by explicit
    // process(42);      // ERROR: would require implicit conversion
    process(UserId{42}); // OK: explicit construction
    return 0;
}

मेंबर इनिशियलाइज़र लिस्ट और इनिशियलाइज़ेशन का क्रम

मेंबर इनिशियलाइज़र लिस्ट (member initializer list) मेंबर को इनिशियलाइज़ करने का पसंदीदा तरीका है। यह const मेंबर, रेफ़रेंस मेंबर और बिना डिफ़ॉल्ट कंस्ट्रक्टर वाले मेंबर के लिए अनिवार्य है। मेंबर डिक्लेयर किए जाने के क्रम में इनिशियलाइज़ होते हैं, उस क्रम में नहीं जिसमें वे इनिशियलाइज़र लिस्ट में दिखते हैं।

init_list.cpp
#include <iostream>
#include <string>

class Connection {
    const int max_retries_;      // const: MUST use init list
    std::string& log_target_;   // reference: MUST use init list
    std::string host_;
    int port_;

public:
    Connection(std::string& log, const std::string& host, int port)
        : max_retries_{3}          // const member
        , log_target_{log}         // reference member
        , host_{host}              // initialized in declaration order
        , port_{port}
    {
        // Constructor body runs AFTER all members are initialized
        log_target_ += "[Connection to " + host_ + " created]\n";
    }

    void print() const {
        std::cout << host_ << ':' << port_
                  << " (max retries: " << max_retries_ << ")\n";
    }
};

int main() {
    std::string log;
    Connection conn{log, "db.example.com", 5432};
    conn.print();
    std::cout << log;
    return 0;
}

डिस्ट्रक्टर और Rule of Zero / Three / Five

डिस्ट्रक्टर (~ClassName()) तब चलता है जब कोई ऑब्जेक्ट नष्ट होता है — यह रिसोर्स मुक्त करता है और उसे कभी एक्सेप्शन नहीं फेंकना चाहिए। अधिकतर क्लास के लिए कंपाइलर द्वारा जनरेट किया गया डिस्ट्रक्टर पर्याप्त होता है।

Rule of Zero: RAII रैपर (std::string, std::vector, std::unique_ptr) को प्राथमिकता दें, ताकि आपको कोई भी स्पेशल मेंबर फ़ंक्शन कभी लिखना ही न पड़े। अगर आपको कोई रॉ रिसोर्स खुद संभालना ही पड़े, तो Rule of Three कहता है कि आपको डिस्ट्रक्टर, कॉपी कंस्ट्रक्टर और कॉपी असाइनमेंट ऑपरेटर चाहिए। Rule of Five इसे बढ़ाकर मूव कंस्ट्रक्टर और मूव असाइनमेंट ऑपरेटर को भी शामिल करता है।

rule_of_five.cpp
#include <iostream>
#include <cstring>

// === Rule of Zero: the GOAL ===
// No special members needed — std::string handles everything
class Person {
    std::string name_;
    int age_;
public:
    Person(const std::string& name, int age)
        : name_{name}, age_{age} {}
    // Compiler generates correct copy/move/destructor automatically!
};

// === Rule of Five: when managing a raw resource ===
class CharBuffer {
    char* data_;
    std::size_t size_;

public:
    explicit CharBuffer(const char* str)
        : size_{std::strlen(str)}
        , data_{new char[std::strlen(str) + 1]}
    {
        std::strcpy(data_, str);
    }

    // Destructor
    ~CharBuffer() { delete[] data_; }

    // Copy constructor (deep copy)
    CharBuffer(const CharBuffer& other)
        : size_{other.size_}
        , data_{new char[other.size_ + 1]}
    {
        std::strcpy(data_, other.data_);
    }

    // Copy assignment (copy-and-swap idiom)
    CharBuffer& operator=(CharBuffer other) {  // note: by value
        swap(*this, other);
        return *this;
    }

    // Move constructor
    CharBuffer(CharBuffer&& other) noexcept
        : data_{other.data_}, size_{other.size_}
    {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // Move assignment
    CharBuffer& operator=(CharBuffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    friend void swap(CharBuffer& a, CharBuffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

    void print() const {
        std::cout << (data_ ? data_ : "(null)") << '\n';
    }
};

int main() {
    CharBuffer a{"Hello"};
    CharBuffer b = a;           // copy constructor
    CharBuffer c{"World"};
    c = a;                      // copy assignment
    CharBuffer d = std::move(a); // move constructor
    d.print();  // Hello
    a.print();  // (null) — moved-from state
    return 0;
}

=default और =delete

आप =default से किसी स्पेशल मेंबर का कंपाइलर-जनरेटेड संस्करण स्पष्ट रूप से माँग सकते हैं, या =delete से उसे स्पष्ट रूप से रोक सकते हैं। इससे आपका इरादा साफ़ रहता है और क्लास की कॉपी/मूव सिमेंटिक्स दस्तावेज़ित हो जाती है।

default_delete.cpp
#include <memory>
#include <string>

// Non-copyable, movable resource handle
class FileHandle {
    std::string path_;
    int fd_;

public:
    FileHandle(const std::string& path, int fd)
        : path_{path}, fd_{fd} {}

    // Explicitly default the destructor
    ~FileHandle() = default;

    // Delete copy operations — file handles should not be duplicated
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // Default move operations
    FileHandle(FileHandle&&) = default;
    FileHandle& operator=(FileHandle&&) = default;
};

int main() {
    FileHandle fh{"data.txt", 3};
    // FileHandle copy = fh;         // ERROR: copy deleted
    FileHandle moved = std::move(fh); // OK: move is allowed
    return 0;
}
सामान्य गलती

मेंबर उस क्रम में इनिशियलाइज़ होते हैं जिसमें वे क्लास में डिक्लेयर किए गए हैं, उस क्रम में नहीं जिसमें वे इनिशियलाइज़र लिस्ट में दिखते हैं। अगर कोई मेंबर किसी दूसरे पर निर्भर है, तो पक्का करें कि जिस पर निर्भरता है वह पहले डिक्लेयर हो। कंपाइलर इस बारे में -Wreorder से चेतावनी देते हैं — उस चेतावनी को कभी अनदेखा न करें। एक क्लासिक बग: size_ को पैरामीटर से इनिशियलाइज़ करना, लेकिन फिर data_ को आवंटित करने के लिए size_ का उपयोग करना, जबकि data_ को size_ से पहले डिक्लेयर किया गया हो।

मुख्य बातें
  • अनचाहे इम्प्लिसिट कन्वर्ज़न रोकने के लिए एकल-आर्गुमेंट कंस्ट्रक्टर पर explicit इस्तेमाल करें
  • मेंबर इनिशियलाइज़र लिस्ट const और रेफ़रेंस मेंबर के लिए अनिवार्य है, और सभी मेंबर के लिए अधिक कुशल भी
  • मेंबर डिक्लेरेशन के क्रम में इनिशियलाइज़ होते हैं, इनिशियलाइज़र-लिस्ट के क्रम में नहीं
  • Rule of Zero का लक्ष्य रखें — RAII टाइप इस्तेमाल करें ताकि कंपाइलर-जनरेटेड स्पेशल मेंबर सही रहें
  • अगर आप डिस्ट्रक्टर/कॉपी-कंस्ट्रक्टर/कॉपी-असाइन में से कोई भी स्वयं लिखते हैं, तो पाँचों स्पेशल मेंबर पर विचार करें और हर एक को स्पष्ट रूप से लिखें, डिफ़ॉल्ट करें या डिलीट करें (Rule of Five)
  • =default इरादे को दस्तावेज़ित करता है; =delete किसी ऑपरेशन को स्पष्ट रूप से प्रतिबंधित करता है

क्विज़ — अपना ज्ञान परखें

(20 XP)

1. कंस्ट्रक्टर पर `explicit` कीवर्ड क्या रोकता है?

2. क्लास के मेंबर किस क्रम में इनिशियलाइज़ होते हैं?

3. Rule of Zero क्या है?