رفتن به محتوا

قالب‌های کلاس و CTAD

ساختارهای داده عمومی را با قالب‌های کلاس بسازید. تعریف اعضا، آرگومان‌های پیش‌فرض و نحوه حذف آرگومان‌های قالب توسط CTAD در ‎C++‎17 را درک کنید.

قالب‌های کلاس (Class Templates)

یک قالب کلاس خانواده‌ای از کلاس‌ها را تعریف می‌کند که با یک یا چند نوع (یا مقادیر غیرنوعی) پارامتری شده‌اند. کتابخانه استاندارد بر پایه قالب‌های کلاس ساخته شده است: std::vector، std::map، std::optional، std::shared_ptr. وقتی std::vector می‌نویسید، کامپایلر کل کلاس را با T = int نمونه‌سازی کرده و یک نوع مشخص تولید می‌کند.

قالب‌های کلاس از همان قواعد قالب‌های تابع پیروی می‌کنند: باید در فایل‌های هدر تعریف شوند و هر مجموعه منحصربه‌فرد از آرگومان‌های قالب یک نوع متمایز تولید می‌کند. Stack و Stack انواع کاملاً نامرتبط هستند — هیچ رابطه ارث‌بری بین آن‌ها وجود ندارد.

یک پشته عمومی (A Generic Stack)

یک پشته عمومی ساده در اینجا آمده است. توجه کنید که توابع عضو تعریف‌شده در خارج از بدنه کلاس نیاز به تکرار لیست پارامتر قالب دارند.

stack.hpp
#include <vector>
#include <stdexcept>
#include <iostream>

template <typename T>
class Stack {
private:
    std::vector<T> elements_;

public:
    void push(const T& value);
    void pop();
    const T& top() const;
    bool empty() const { return elements_.empty(); }
    std::size_t size() const { return elements_.size(); }
};

// Member definitions outside the class require the template header
template <typename T>
void Stack<T>::push(const T& value) {
    elements_.push_back(value);
}

template <typename T>
void Stack<T>::pop() {
    if (elements_.empty()) {
        throw std::out_of_range("Stack<>::pop(): empty stack");
    }
    elements_.pop_back();
}

template <typename T>
const T& Stack<T>::top() const {
    if (elements_.empty()) {
        throw std::out_of_range("Stack<>::top(): empty stack");
    }
    return elements_.back();
}

int main() {
    Stack<int> intStack;
    intStack.push(42);
    intStack.push(17);
    std::cout << intStack.top() << '\n'; // 17
    intStack.pop();
    std::cout << intStack.top() << '\n'; // 42
}

پارامترهای متعدد و مقادیر پیش‌فرض

قالب‌های کلاس می‌توانند پارامترهای قالب متعدد و آرگومان‌های قالب پیش‌فرض داشته باشند، درست مانند پارامترهای تابع که می‌توانند مقادیر پیش‌فرض داشته باشند. آرگومان‌های پیش‌فرض از چپ به راست حل می‌شوند.

stack_defaults.cpp
#include <vector>
#include <deque>
#include <iostream>

// Container defaults to std::deque<T> — matches std::stack's design
template <typename T, typename Container = std::deque<T>>
class Stack {
private:
    Container elements_;

public:
    void push(const T& value) { elements_.push_back(value); }
    void pop()                { elements_.pop_back(); }
    const T& top() const      { return elements_.back(); }
    bool empty() const        { return elements_.empty(); }
};

int main() {
    Stack<int> s1;                         // uses deque<int>
    Stack<int, std::vector<int>> s2;       // uses vector<int>

    s1.push(10);
    s2.push(20);
    std::cout << s1.top() << '\n'; // 10
    std::cout << s2.top() << '\n'; // 20
}

استنتاج آرگومان قالب کلاس — CTAD

پیش از ‎C++‎17، همیشه باید آرگومان‌های قالب را هنگام ساخت نمونه‌های قالب کلاس مشخص می‌کردید: std::pair p(1, 3.14);. CTAD (استنتاج آرگومان قالب کلاس)، که در ‎C++‎17 معرفی شد، به کامپایلر اجازه می‌دهد آرگومان‌های قالب کلاس را از آرگومان‌های سازنده استنتاج کند، درست مانند استنتاج آرگومان قالب تابع.

با CTAD: std::pair p(1, 3.14); — کامپایلر std::pair را استنتاج می‌کند. این برای std::vector، std::tuple، std::optional، std::array (‎C++‎20) و قالب‌های کلاس خودتان کار می‌کند.

CTAD از راهنماهای استنتاج (deduction guides) استفاده می‌کند — چه ضمنی (تولیدشده از سازنده‌ها) و چه صریح (تعریف‌شده توسط کاربر). کامپایلر هر راهنما را امتحان کرده و بهترین تطابق را انتخاب می‌کند.

CTAD و راهنماهای استنتاج

راهنماهای استنتاج تعریف‌شده توسط کاربر به کامپایلر نشان می‌دهند چگونه آرگومان‌های سازنده را به آرگومان‌های قالب نگاشت کند. آن‌ها به‌ویژه زمانی مفید هستند که نوع آرگومان سازنده مستقیماً با پارامتر قالب مطابقت نداشته باشد.

ctad_example.cpp
#include <iostream>
#include <cstddef>

template <typename T, std::size_t N>
struct Array {
    T data[N];
};

// Without a deduction guide, CTAD cannot deduce T and N.
// This explicit deduction guide maps brace-init with N elements
// of the same type T to Array<T, N>.
template <typename First, typename... Rest>
Array(First, Rest...) -> Array<First, 1 + sizeof...(Rest)>;

// Standard library example: std::vector CTAD from iterators
#include <vector>
#include <list>

int main() {
    // CTAD with our custom deduction guide
    Array arr{1, 2, 3, 4, 5};  // deduces Array<int, 5>
    std::cout << arr.data[2] << '\n'; // 3

    // Standard library CTAD examples (C++17)
    std::pair p(42, 3.14);       // pair<int, double>
    std::vector v{1, 2, 3};     // vector<int>
    std::tuple t(1, 2.0, 'a');  // tuple<int, double, char>

    // CTAD from iterator range
    std::list<int> lst{10, 20, 30};
    std::vector v2(lst.begin(), lst.end()); // vector<int>
}

قالب‌های عضو (Member Templates)

توابع عضو یک قالب کلاس خود می‌توانند قالب باشند و پارامترهای قالب اضافی داشته باشند. به این قالب عضو (member template) گفته می‌شود. یک مورد استفاده رایج فعال‌سازی انتساب یا ساخت از یک نمونه‌سازی مرتبط اما متفاوت است.

به‌عنوان مثال، ممکن است بخواهید یک Stack را از Stack مقداردهی کنید — اما چون این‌ها انواع نامرتبط هستند، عملگر انتساب کپی پیش‌فرض کار نخواهد کرد. یک عملگر انتساب قالب عضو این مشکل را حل می‌کند:

template <typename T>
class Stack {
public:
    template <typename U>
    Stack& operator=(const Stack<U>& other);
};

در داخل این قالب عضو، U ممکن است با T متفاوت باشد. بدنه می‌تواند عناصر را از other کپی کند به شرطی که U قابل تبدیل به T باشد. این الگو به‌طور گسترده در کتابخانه استاندارد استفاده می‌شود — std::shared_ptr از طریق یک سازنده قالب عضو به std::shared_ptr تبدیل می‌شود.

دام

وقتی یک قالب کلاس را تخصص می‌دهید، تخصص یک کلاس کاملاً جدید است — هیچ عضوی از قالب اصلی به ارث نمی‌برد. باید هر تابع عضو و عضو داده را مجدداً تعریف کنید. فراموش کردن تعریف مجدد یک عضو در تخصص منجر به خطای کامپایل هنگام استفاده از آن عضو می‌شود. همچنین، تخصص جزئی فقط برای قالب‌های کلاس در دسترس است، نه قالب‌های تابع. تخصص را به‌طور مفصل در فصل بعدی بررسی خواهیم کرد.

نکات کلیدی
  • قالب‌های کلاس خانواده‌ای از کلاس‌ها را تعریف می‌کنند — هر مجموعه آرگومان منحصربه‌فرد یک نوع متمایز تولید می‌کند
  • توابع عضو تعریف‌شده در خارج از کلاس باید هدر template را تکرار کرده و از ClassName:: استفاده کنند
  • آرگومان‌های قالب پیش‌فرض از چپ به راست کار می‌کنند، درست مانند آرگومان‌های پیش‌فرض تابع
  • CTAD (‎C++‎17) به کامپایلر اجازه می‌دهد آرگومان‌های قالب کلاس را از آرگومان‌های سازنده استنتاج کند
  • راهنماهای استنتاج صریح الگوهای آرگومان سازنده را به آرگومان‌های قالب نگاشت می‌کنند
  • قالب‌های عضو عملیات بین نمونه‌سازی‌های مختلف یک قالب کلاس را امکان‌پذیر می‌کنند

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

(15 امتیاز)

1. با توجه به `template <typename T, typename Container = std::deque<T>> class Stack`، نوع `Stack<int>` چیست؟

2. CTAD (‎C++‎17) چیست؟

3. وقتی یک قالب کلاس را تخصص می‌دهید، چه اتفاقی برای اعضای قالب اصلی می‌افتد؟