رفتن به محتوا

قالب‌های تابع و استنتاج نوع (Function Templates & Type Deduction)

یاد بگیرید چگونه توابعی بنویسید که با هر نوعی کار کنند. استنتاج آرگومان قالب، پارامترهای غیرنوعی و دلیل قرار گرفتن قالب‌ها در فایل‌های هدر را درک کنید.

چرا قالب‌های تابع؟

بدون قالب‌ها، نوشتن تابعی که برای انواع مختلف کار کند به معنای تکرار منطق است. شما باید int max(int, int)، double max(double, double)، std::string max(std::string, std::string) بنویسید — منطق یکسانی که برای هر نوع تکرار می‌شود. قالب‌های تابع به شما اجازه می‌دهند الگوریتم را یک بار بنویسید و به کامپایلر اجازه دهید نسخه‌های مخصوص هر نوع را به‌طور خودکار تولید کند.

قالب تابع یک تابع نیست — بلکه یک الگو (blueprint) است که کامپایلر از روی آن وقتی از آن با انواع خاص استفاده می‌کنید، توابع واقعی را نمونه‌سازی (instantiate) می‌کند. این فرایند کاملاً در زمان کامپایل انجام می‌شود و کدی تولید می‌کند که دقیقاً مشابه چیزی است که خودتان می‌نوشتید — بدون هیچ سرباری.

نحو قالب (Template Syntax)

قالب تابع با کلمه کلیدی template و به دنبال آن لیست پارامترها در کروشه‌های زاویه‌ای معرفی می‌شود. می‌توانید از typename یا class به‌جای یکدیگر برای پارامترهای نوع استفاده کنید — هر دو دقیقاً معنای یکسانی دارند. قرارداد مدرن typename را ترجیح می‌دهد.

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

// 'typename' and 'class' are interchangeable here
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Equivalent — 'class' means the same thing
template <class T>
T minimum(T a, T b) {
    return (a < b) ? a : b;
}

int main() {
    std::cout << maximum(3, 7)          << '\n'; // deduces T = int
    std::cout << maximum(3.14, 2.71)    << '\n'; // deduces T = double
    std::cout << maximum<std::string>("hello", "world") << '\n';
    // explicit instantiation needed — "hello" is const char*, not std::string
}

قواعد استنتاج آرگومان قالب

کامپایلر آرگومان‌های قالب را با تطبیق انواع آرگومان‌های تابع با الگوهای پارامتر استنتاج می‌کند. قواعد کلیدی عبارتند از:

1. تطابق دقیق ترجیح داده می‌شود — maximum(3, 7) نوع T = int را استنتاج می‌کند زیرا هر دو آرگومان int هستند.
2. هیچ تبدیل ضمنی در حین استنتاج انجام نمی‌شود — maximum(3, 7.0) یک خطا است: کامپایلر از آرگومان اول T = int و از آرگومان دوم T = double را می‌بیند و از انتخاب یکی امتناع می‌کند.
3. تبدیل آرایه به اشاره‌گر — وقتی T به‌عنوان پارامتر مقداری ظاهر شود، آرایه‌ها به اشاره‌گر تبدیل می‌شوند. از T(&)[N] برای ثبت نوع و اندازه آرایه استفاده کنید.
4. const/volatile سطح بالا حذف می‌شود از پارامترهای مقداری اما برای پارامترهای مرجعی/اشاره‌گری حفظ می‌شود.
5. جمع شدن مرجع (Reference collapsing) — اگر T به‌عنوان نوع مرجعی استنتاج شود (در مراجع ارجاع‌دهنده)، قواعد جمع شدن مرجع اعمال می‌شود: T& & به T&، T& && به T&، T&& & به T&، T&& && به T&& تبدیل می‌شود.

وقتی استنتاج شکست بخورد یا مبهم باشد، باید آرگومان‌های قالب را به‌صراحت مشخص کنید: maximum(3, 7.0).

پارامترهای قالب غیرنوعی (Non-Type Template Parameters)

پارامترهای قالب محدود به انواع نیستند. پارامترهای قالب غیرنوعی (NTTPs) مقادیر زمان کامپایل مانند اعداد صحیح، اشاره‌گرها، یا (از ‎C++‎20 به بعد) انواع اعشاری و کلاس‌ها را می‌پذیرند. آن‌ها برای جاسازی ثابت‌های زمان کامپایل در سیستم نوع بسیار قدرتمند هستند.

non_type_params.cpp
#include <iostream>
#include <array>
#include <cstddef>

// Non-type parameter: N is a compile-time size_t value
template <typename T, std::size_t N>
T sum(const std::array<T, N>& arr) {
    T total{};
    for (const auto& elem : arr) {
        total += elem;
    }
    return total;
}

// C++20: auto non-type parameter accepts any structural type
template <auto Value>
constexpr auto doubled = Value * 2;

int main() {
    std::array<int, 4> nums = {1, 2, 3, 4};
    std::cout << sum(nums) << '\n';     // 10, deduces T=int, N=4

    std::cout << doubled<21> << '\n';   // 42
    std::cout << doubled<3.14> << '\n'; // 6.28 (C++20)
}
توجه

هر ترکیب منحصربه‌فرد از آرگومان‌های قالب باعث می‌شود کامپایلر یک نمونه‌سازی جداگانه — یک کپی مجزا از تابع در فایل باینری شما — تولید کند. فراخوانی maximum، maximum و maximum سه تابع تولید می‌کند. این معمولاً مشکلی ندارد، اما در پایگاه‌های کد بزرگ، قالب‌ها می‌توانند به تورم کد (باینری‌های بزرگ‌تر، زمان کامپایل طولانی‌تر) منجر شوند. راهبردهای کاهش شامل: استفاده از اعلان‌های extern template برای کنترل محل وقوع نمونه‌سازی‌ها، استخراج کد مستقل از نوع از قالب‌ها، و استفاده از محو نوع (type erasure) وقتی چندریختی زمان اجرا قابل‌قبول باشد.

دام

از آنجا که کامپایلر برای نمونه‌سازی قالب نیاز به دیدن تعریف کامل آن دارد، قالب‌ها نمی‌توانند مانند توابع معمولی به فایل‌های .h / .cpp تقسیم شوند. اگر تعریف قالب را در فایل .cpp قرار دهید و سعی کنید از واحد ترجمه دیگری از آن استفاده کنید، یک خطای لینکر (مرجع تعریف‌نشده) دریافت خواهید کرد. راه‌حل استاندارد این است که کل تعریف قالب را در فایل هدر قرار دهید. اگر زمان کامپایل مشکل‌ساز شد، می‌توانید از نمونه‌سازی صریح استفاده کنید: تعریف را در فایل .cpp قرار دهید و template int maximum(int, int); را برای نمونه‌سازی صریح تخصص‌های مورد نیاز اضافه کنید.

نمونه‌سازی صریح (Explicit Instantiation)

می‌توانید به‌صراحت محل و زمان نمونه‌سازی قالب‌ها را کنترل کنید. این می‌تواند با جلوگیری از نمونه‌سازی‌های تکراری در واحدهای ترجمه مختلف، زمان کامپایل را در پروژه‌های بزرگ کاهش دهد.

explicit_instantiation.h
// ---- maximum.h ----
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Suppress implicit instantiation in every TU that includes this header
extern template int maximum<int>(int, int);
extern template double maximum<double>(double, double);

// ---- maximum.cpp ----
#include "maximum.h"

// Explicit instantiation: generate code here, once
template int maximum<int>(int, int);
template double maximum<double>(double, double);
نکات کلیدی
  • قالب‌های تابع الگوهایی هستند — کامپایلر توابع مشخصی را برای هر مجموعه آرگومان قالب تولید می‌کند
  • typename و class در لیست پارامترهای قالب قابل جایگزینی هستند
  • استنتاج آرگومان قالب تبدیل‌های ضمنی انجام نمی‌دهد — استنتاج‌های مبهم خطا هستند
  • پارامترهای قالب غیرنوعی مقادیر زمان کامپایل را در سیستم نوع جاسازی می‌کنند
  • قالب‌ها باید در فایل‌های هدر تعریف شوند (یا از نمونه‌سازی صریح استفاده شود) زیرا کامپایلر برای نمونه‌سازی به تعریف کامل نیاز دارد

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

(15 امتیاز)

1. وقتی `maximum(3, 7.0)` را با قالب `template<typename T> T maximum(T a, T b)` فراخوانی کنید، چه اتفاقی می‌افتد؟

2. چرا تعریف قالب‌ها معمولاً باید در فایل‌های هدر قرار گیرد؟

3. هدف `extern template int maximum<int>(int, int);` چیست؟