تخصص قالب و ارسال برچسب (Template Specialization & Tag Dispatch)
تخصص کامل و جزئی را تسلط یابید، بیاموزید چه زمانی بارگذاری مجدد ترجیح دارد، و ارسال برچسب و if constexpr را بهعنوان جایگزینهایی برای انشعاب زمان کامپایل کشف کنید.
چرا تخصص؟
قالب اصلی یک پیادهسازی عمومی ارائه میدهد که برای اکثر انواع کار میکند. اما گاهی یک نوع خاص نیاز به رفتار اساساً متفاوتی دارد. بهعنوان مثال، std::vector یک تخصص است که بولها را برای بهرهوری فضا در بیتها فشرده میکند (هرچند بهطور گسترده بهعنوان اشتباه در کتابخانه استاندارد در نظر گرفته میشود).
تخصص قالب به شما اجازه میدهد تعریف جایگزینی برای آرگومانهای قالب خاص ارائه دهید. دو نوع وجود دارد:
1. تخصص کامل (صریح) — تمام پارامترهای قالب را به انواع خاص تثبیت میکنید.
2. تخصص جزئی — برخی پارامترها را تثبیت یا محدود میکنید (مثلاً تخصص برای تمام انواع اشارهگر). تخصص جزئی فقط برای قالبهای کلاس در دسترس است، نه قالبهای تابع.
تخصص کامل (Full Specialization)
تخصص کامل یک پیادهسازی کاملاً سفارشی برای یک مجموعه خاص از آرگومانهای قالب ارائه میدهد. نحو از template <> (لیست پارامتر خالی) و به دنبال آن کلاس یا تابع با آرگومانهای مشخص استفاده میکند.
#include <iostream>
#include <cstring>
// Primary template
template <typename T>
class Formatter {
public:
static std::string format(const T& value) {
return std::to_string(value);
}
};
// Full specialization for const char*
template <>
class Formatter<const char*> {
public:
static std::string format(const char* value) {
return std::string("\"" ) + value + "\"";
}
};
// Full specialization for bool
template <>
class Formatter<bool> {
public:
static std::string format(bool value) {
return value ? "true" : "false";
}
};
int main() {
std::cout << Formatter<int>::format(42) << '\n'; // 42
std::cout << Formatter<const char*>::format("hi") << '\n'; // "hi"
std::cout << Formatter<bool>::format(true) << '\n'; // true
}تخصص جزئی (Partial Specialization)
تخصص جزئی برخی پارامترهای قالب را محدود میکند در حالی که بقیه را عمومی نگه میدارد. این برای تطبیق خانوادههای انواع — تمام اشارهگرها، تمام آرایهها، تمام جفتها و غیره — بسیار قدرتمند است. فقط قالبهای class/struct/variable از تخصص جزئی پشتیبانی میکنند.
#include <iostream>
#include <string>
#include <typeinfo>
// Primary template
template <typename T>
struct TypeInfo {
static std::string name() { return "general type"; }
};
// Partial specialization for all pointer types
template <typename T>
struct TypeInfo<T*> {
static std::string name() {
return "pointer to " + TypeInfo<T>::name();
}
};
// Partial specialization for all references
template <typename T>
struct TypeInfo<T&> {
static std::string name() {
return "reference to " + TypeInfo<T>::name();
}
};
// Partial specialization for arrays
template <typename T, std::size_t N>
struct TypeInfo<T[N]> {
static std::string name() {
return "array of " + std::to_string(N) + " " + TypeInfo<T>::name();
}
};
int main() {
std::cout << TypeInfo<int>::name() << '\n'; // general type
std::cout << TypeInfo<int*>::name() << '\n'; // pointer to general type
std::cout << TypeInfo<int**>::name() << '\n'; // pointer to pointer to general type
std::cout << TypeInfo<int[5]>::name() << '\n'; // array of 5 general type
}قالبهای تابع نمیتوانند بهصورت جزئی تخصص داده شوند — فقط بهصورت کامل. بدتر از آن، تخصصهای قالب تابع در تشخیص بارگذاری مجدد آنطور که انتظار دارید شرکت نمیکنند. کامپایلر ابتدا بهترین قالب پایه را از طریق تشخیص بارگذاری مجدد انتخاب میکند و فقط سپس بررسی میکند آیا آن قالب پایه تخصص مطابقی دارد.
این منجر به رفتار شگفتانگیزی میشود:
template <typename T> void f(T) { /* #1 - primary */ }
template <typename T> void f(T*) { /* #2 - overload for pointers */ }
template <> void f<int*>(int*) { /* #3 - specialization of #1 */ }فراخوانی f((int*)nullptr) شماره #2 (تطابق بارگذاری مجدد بهتر) را انتخاب میکند، نه #3 (که #1 را تخصص میدهد، نه #2). راهحل: از بارگذاری مجدد تابع معمولی بهجای تخصص قالب تابع استفاده کنید. اگر به انشعاب نوع زمان کامپایل نیاز دارید، از if constexpr یا مفاهیم (concepts) استفاده کنید.
الگوی ارسال برچسب (Tag Dispatch Pattern)
ارسال برچسب از انواع struct خالی («برچسبها») برای انتخاب بارگذاری مجدد در زمان کامپایل استفاده میکند. کتابخانه استاندارد از این روش بهطور گسترده استفاده میکند — std::advance بر اساس برچسبهای دسته تکرارکننده ارسال میکند. این تکنیک غالب انشعاب زمان کامپایل پیش از if constexpr بود.
#include <iostream>
#include <iterator>
#include <vector>
#include <list>
namespace detail {
// Tags are empty structs used purely for overload selection
template <typename Iter>
void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
it += n; // O(1) — random access
std::cout << "random access advance\n";
}
template <typename Iter>
void advance_impl(Iter& it, int n, std::bidirectional_iterator_tag) {
// O(n) — step one at a time, forward or backward
while (n > 0) { ++it; --n; }
while (n < 0) { --it; ++n; }
std::cout << "bidirectional advance\n";
}
template <typename Iter>
void advance_impl(Iter& it, int n, std::input_iterator_tag) {
while (n > 0) { ++it; --n; }
std::cout << "input iterator advance\n";
}
}
template <typename Iter>
void my_advance(Iter& it, int n) {
// iterator_traits extracts the category tag
detail::advance_impl(it, n,
typename std::iterator_traits<Iter>::iterator_category{});
}
int main() {
std::vector<int> v{1, 2, 3, 4, 5};
auto vit = v.begin();
my_advance(vit, 3); // random access advance
std::list<int> l{1, 2, 3, 4, 5};
auto lit = l.begin();
my_advance(lit, 3); // bidirectional advance
}if constexpr: جایگزین مدرن
if constexpr در C++17 راهی تمیزتر برای انشعاب در زمان کامپایل درون یک تابع واحد فراهم میکند. شاخه کنار گذاشتهشده نمونهسازی نمیشود، بنابراین لازم نیست برای نوع دادهشده کامپایل شود. این بهطور عمده جایگزین ارسال برچسب و SFINAE برای موارد ساده شده است.
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>
template <typename T>
std::string stringify(const T& value) {
if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(value);
} else if constexpr (std::is_same_v<T, std::string>) {
return '"' + value + '"';
} else if constexpr (std::is_same_v<T, const char*>) {
return std::string("\"" ) + value + "\"";
} else {
return "[non-printable]";
}
}
int main() {
std::cout << stringify(42) << '\n'; // 42
std::cout << stringify(3.14) << '\n'; // 3.140000
std::cout << stringify(std::string("hello")) << '\n'; // "hello"
std::cout << stringify("world") << '\n'; // "world"
}از این راهنمای تصمیمگیری برای انشعاب زمان کامپایل استفاده کنید:
- if constexpr (C++17) — سادهترین، درون یک تابع واحد کار میکند، نیاز به الگوهای خاصی ندارد. برای منطق ساده مبتنی بر نوع این را ترجیح دهید.
- مفاهیم/requires (C++20) — بهترین برای محدود کردن بارگذاری مجدد توابع یا کل قالبها. خوداسنادی و پیامهای خطای واضح ارائه میدهد.
- ارسال برچسب — هنوز وقتی سلسلهمراتب برچسب موجود دارید (مانند دستههای تکرارکننده) مفید است. پایگاههای کد قبل از C++17 بهشدت به این وابسته هستند.
- SFINAE / enable_if — قدیمیترین تکنیک. خوانایی سختتر، خطاهای رمزآلود. در کد جدید اگر مفاهیم یا if constexpr کار میکنند از آن اجتناب کنید.
- تخصص کامل — برای قالبهای کلاس وقتی به پیادهسازی کاملاً متفاوت برای یک نوع خاص نیاز دارید استفاده کنید.
- هرگز از تخصص قالب تابع استفاده نکنید — بهجای آن از بارگذاری مجدد استفاده کنید.
- تخصص کامل (
template <>) یک پیادهسازی سفارشی برای یک نوع خاص ارائه میدهد - تخصص جزئی برخی پارامترها را محدود میکند — فقط برای قالبهای کلاس در دسترس است، نه قالبهای تابع
- تخصص قالب تابع مستعد خطا است — بارگذاری مجدد یا
if constexprرا ترجیح دهید - ارسال برچسب از انواع struct خالی برای انتخاب بارگذاری مجدد در زمان کامپایل از طریق
iterator_traitsاستفاده میکند if constexpr(C++17) جایگزین مدرن و تمیزتری است که شاخههای انتخابنشده را در زمان کامپایل کنار میگذارد
آزمون — دانش خود را بسنجید
(20 امتیاز)1. چرا باید از تخصص قالبهای تابع اجتناب کنید؟
2. کدام انواع قالبها از تخصص جزئی پشتیبانی میکنند؟
3. `if constexpr` چه تفاوتی با `if` معمولی دارد؟