رفتن به محتوا

سازنده‌ها، مخرب‌ها و قانون 0/3/5

بر چرخه حیات کامل اشیاء ‎C++‎ — از ساخت تا تخریب — مسلط شوید و یاد بگیرید چه زمانی توابع عضو ویژه بنویسید (یا ننویسید).

چرخه حیات شیء در ‎C++‎

در ‎C++‎، شما کنترل مستقیم بر زمان و نحوه ایجاد و تخریب اشیاء دارید. کامپایلر می‌تواند تا شش تابع عضو ویژه را به‌صورت خودکار تولید کند: سازنده پیش‌فرض، سازنده کپی، عملگر تخصیص کپی، سازنده انتقال، عملگر تخصیص انتقال و مخرب. درک اینکه چه زمانی خودتان بنویسید — و چه زمانی به کامپایلر اجازه دهید — یکی از مهم‌ترین مهارت‌ها در ‎C++‎ است.

انواع سازنده‌ها

سازنده‌ها (Constructors) یک شیء را مقداردهی اولیه می‌کنند. ‎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;
}

مخرب‌ها و قانون صفر / سه / پنج

یک مخرب (Destructor) (~ClassName()) هنگام تخریب یک شیء اجرا می‌شود — منابع را آزاد می‌کند و نباید استثنا پرتاب کند. برای اکثر کلاس‌ها، مخرب تولیدشده توسط کامپایلر کافی است.

قانون صفر: پوشش‌دهنده‌های RAII (std::string، std::vector، std::unique_ptr) را ترجیح دهید تا هرگز نیازی به نوشتن هیچ تابع عضو ویژه‌ای نداشته باشید. اگر باید یک منبع خام را مدیریت کنید، قانون سه می‌گوید که به مخرب، سازنده کپی و عملگر تخصیص کپی نیاز دارید. قانون پنج این را به شامل سازنده انتقال و عملگر تخصیص انتقال گسترش می‌دهد.

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_ از یک پارامتر و سپس استفاده از size_ برای تخصیص data_، در حالی که data_ قبل از size_ اعلان شده است.

نکات کلیدی
  • از explicit روی سازنده‌های تک‌آرگومانی برای جلوگیری از تبدیل‌های ضمنی ناخواسته استفاده کنید
  • لیست مقداردهنده عضو برای اعضای const و ارجاعی الزامی و برای همه اعضا کارآمدتر است
  • اعضا به ترتیب اعلان مقداردهی می‌شوند، نه ترتیب لیست مقداردهنده
  • هدف قانون صفر باشد — از انواع RAII استفاده کنید تا اعضای ویژه تولیدشده توسط کامپایلر صحیح باشند
  • اگر هر یک از مخرب/سازنده‌کپی/تخصیص‌کپی را نوشتید، هر پنج تا را بنویسید (قانون پنج)
  • =default نیت را مستند می‌کند؛ =delete صریحاً یک عملیات را منع می‌کند

آزمون — دانش خود را بسنجید

(20 امتیاز)

1. کلمه کلیدی `explicit` روی یک سازنده از چه چیزی جلوگیری می‌کند؟

2. اعضای کلاس به چه ترتیبی مقداردهی اولیه می‌شوند؟

3. قانون صفر (Rule of Zero) چیست؟