زبان C++ یک زبان همه منظوره و بسیار سریع هست و خیلی امکانات جالبی داره. یکی از این ویژگی ها template ها هستند. اگر قبلا با زبان های static typed مثل java و #C کار کرده باشید، حتما با مفهوم Generic ها آشنا هستید و می دونید که به چه منظور استفاده میشن.
در C++ هم template ها مفهومی شبیه به generic ها دارند اما تفاوت های اساسی بین اینها وجود داره که در ادامه به آنها اشاره خواهیم کرد.
فرض کنید که تابعی داریم که قراره دو تا عدد رو با هم جمع کنه. از اونجایی که C++ سیستم static typed دارد، برای هر نوع خاصی از عدد باید یک overload از روی تابع اصلی ایجاد کنیم:
اگر به تابع بالا نگاهی بیندازیم، الگوی زیر رو مشاهده می کنیم:
خوب اگر تنها چیزی که قراره تغییر کنه نوع مقادیر ورودی و مقدار خروجی هست، چرا نتونیم این دو تابع رو یکی کنیم؟ درست حدس زدید. template ها بهمون دقیقا همین قابلیت رو میدهند. حالا اگر تابع add رو به روش زیر بازنویسی کنیم، می تونیم هر نوع عددی رو دریافت، جمع و به عنوان نتیجه برگردونیم:
اگر با Macro ها در C++ آشنا باشید، می دونید که از macro ها برای جایگذاری متن هنگام کامپایل استفاده میشه. template ها خیلی به macro ها شبیه هستند. template ها یه سری آرگومان میگیرند که بهشون میگن template arguments و بر اساس اون آرگومان ها یک تابع جدید، یک Struct جدید یا یک class جدید در زمان کامپایل ایجاد می کنند.
در مثال بالا، اگر بخواهید تابع add را در جایی از کد هاتون فراخوانی کنید باید به این شکل این کار رو انجام بدید:
add<int>(10, 20);
پارامتر int که بین <> قرار داره، مشخص می کنه که تابع در ادامه چه نوع داده ای رو قبول می کنه. البته در C++17 ویژگی به نام implicit template argument deduction اضافه شده که بر اساس اون، درصورتی که نوع template argument ها از روی پارامتر های داده شده به تابع قابل تشخیص باشه، دیگه نیازی به مشخص کردن صریح template argument ها بین <> نیست.
مثلا در C++17 می تونید تابع add رو مثل هر تابع معمولی دیگه ای فراخوانی کنید و کامپایلر C++ خودش تشخیص میده که نوع داده استفاده شده چی هست:
add(10, 20);
همچنین می تونید از template ها در کلاس ها به این صورت استفاده کنید
تنها نکته ای که می خوام در کد بالا بهش اشاره کنم عبارت های using هستش. اگر قبلا با زبان C کار کرده باشید حتما می دونید typedef چی هست. عبارت های using بالا هم دقیقا کار typedef رو انجام میدن.
مثلا اگر بنویسیم:
using Number_Type = int;
و بعد متغیری از نوع Number_Type تعریف کنیم، دقیقا مثل این هست که یک int تعریف کرده باشیم:
Number_Type n1 = 45; int n1 = 45; // Both are equivalent
عبارت هایی که با کلیدواژه using ساخته میشن، در موقعیت های مختلف استفاده های مختلفی دارند:
من قرار نیست راجع به این ها در این مقاله توضیح بدم چون از موضوع بحث ما خارجه. اما اگر می خواهید راجع به اونها بدونید می تونید به این مقاله مراجعه کنید
خوب در struct ها هم روند کار دقیقا به همین صورت هست:
شاید فکر کنید که template ها فقط قادر به گرفتن نوع داده به عنوان آرگومان هستند. ولی کاملا در اشتباه هستید. چون شما می تونید از constant ها و literal ها نیز به عنوان آرگومان استفاده کنید. بزارید بهتون با یک مثال ساده نشون بدم:
همونطور که میبینید، آرگومان size از نوع int هست. حالا میتونید به این شکل از روی DataContainer یک obj ایجاد کنید:
می تونید یک literal یا یک constant به عنوان size به template بدید ولی استفاده از یک متغیر مجاز نیست. چون مقدار size باید در زمان کامپایل مشخص و در زمان اجرا ثابت باشه که متغیر ها همچین ضمانتی ندارند.
با template ها میشه الگوریتمی نوشت که در زمان کامپایل اجرا بشه و اینطوری کمی از بار پردازش زمان اجرا میشه کم کرد.
حالا برای محاسبه فاکتوریل عدد 5 میشه همچین چیزی نوشت:
unsigned int result = factorial<5>::value
مبحث metaprogramming مقوله خیلی گسترده ای هست. برای اطلاعات بیشتر می تونید به این لینک مراجعه کنید.
شما می تونید رفتار خاصی برای نوع خاصی از داده ها در نظر بگیرید. مثلا فرض کنید که تابعی به نام sort داریم. اگر ورودی تابع عدد باشه، اعداد رو باید از کوچیک به بزرگ مرتب کنه. اگر ورودی string باشه، باید به ترتیب حروف الفبا مرتبش کنه. اگر ورودی یک تاریخ باشه، باید اون رو بر اساس روز، ماه و سال مرتب کنه:
اگر توجه کرده باشید، در مثالی که کمی قبل تر برای محاسبه factorial زدیم، از template specialization نیز استفاده کردیم که زمانی که مقدار آرگومان 0 بود، value برابر 1 باشه و دیگه در یک حلقه بی نهایت نیوفتیم. اگر می خواهید راجع به این مبحث بیشتر بدونید، می تونید به این لینک مراجعه کنید.
تا به حال از خودتون پرسیده اید که چطور توابعی مانند printf می تونن تعداد بی نهایت آرگومان به عنوان ورودی دریافت کنند؟ template ها به شما این اجازه رو میدن که به تعداد بی نهایت template argument دریافت کنید:
برای گرفتن بقیه آرگومان ها باید از این syntax استفاده کنید:
template <typename FirstParameter, typename... RestOfParameters> FirstParameter function_name(FirstParameter first_value, RestOfParameters... rest_values);
حالا شاید بپرسید که چرا FirstParameter رو بر نمیداریم تا کلا از RestOfParameters استفاده کنیم؟ دلیلش اینه که C++ به ما اجازه نمیده به type های داخل RestOfParameters دسترسی داشته باشیم. به خاطر همین مجبوریم، اونها رو یکی یکی از طریق FirstParameter دریافت کنیم و یکی یکی پردازش کنیم. اما چطور همچین اتفاقی می افته؟
به مثالی که در بالا زده ام توجه کنید. تابع Add رو می تونید با هر تعداد عدد دلخواه فراخوانی کنید و جمع همه اعداد رو تحویل بگیرید. نحوه فراخوانی به صورت زیر است:
Add <int, int, int> (10, 20, 30);
هر چقدر تایپ های بیشتری رو مشخص کنید، می تونید عدد های بیشتری پاس بدید. البته در C++17 دیگه لازم نیست همچین کاری بکنید، به لطف implicit template argument deduction می تونید بدون مشخص کردن تایپ ها هر چقدر دلتون خواست عدد بدید:
Add (10, 20, 30);
وقتی شما تابع رو فراخوانی می کنید، همچین چیزی اتفاق می افته:
Add (10, 20, 30); // First [int] -> first template argument // f = 10 [int] -> first function argument // Rest [int, int] -> rest of template arguments // args = 20 [int], 30 [int] -> rest of function arguments
اگر توجه کنید، از همه آرگومان هایی که به تابع داده شده، یکی از اونها در یک متغیر جدا قرار گرفته. از اونجایی که ما داریم از recursive call استفاده می کنیم، این رویه همینطور ادامه پیدا میکنه. و هر بار یکی از از Rest و یکی از args جدا میشن و در به ترتیب در First و f قرار میگیرن. به طوری که First نوع f رو مشخص می کنه و خود f هم مقدار رو ذخیره می کنه:
Add(10, 20, 30, 40) = 10 + Add(20, 30, 40) = 10 + 20 + Add(30, 40) = ... = 10 + 20 + 30 + 40
خیلی چیز ها راجع به variadic template ها وجود داره. اگر می خواهید بیشتر راجع بهشون بدونید، حتما به این لینک مراجعه کنید.
در مثال اول، وقتی شما تابع add رو به صورت یک template تعریف میکنید، درواقع هیچ تابعی تعریف نمیشه! پس چی میشه؟ هیچی نمیشه! :-|)
البته تا زمانی که به کامپایلر بگید از روی template (یا قالب) ای که بهش دادید، یک نسخه از تابع add رو بسازه. به طور کلی دو راه برای این کار وجود داره:
در روش اول شما به آسونی در جایی از کد، تابع خود را فراخوانی می کنید و کامپایلر overload های مختلفی از تابع مورد نظر رو ایجاد می کنه:
ولی در روش دوم باید به شکل زیر به کامپایلر مستقیما اعلام کنید که باید نسخه های مورد نیاز از روی add ایجاد بشه. اینکه این روش به چه دردی می خوره و چرا باید از همچین چیزی استفاده کنیم در بخش بعدی مطلب ذکر شده پس فعلا زیاد فکر خودتون رو درگیرش نکنید:
و برای کلاس ها اینطور باید عمل کنیم:
در C++ به هر فایلcpp که قراره تبدیل به یک object file بشه میگن translation unit. اگر یادتون باشه، گفته شد که template ها واقعی نیستند. پس اگر بدون استفاده از اونها فایل هامون رو compile کنیم، یعنی مثلا یک فایل lib برای استفاده در دیگر پروژه هامون بسازیم، با یک مشکل بزرگ مواجه میشیم. چون کد های پیاده سازی template ها در فایل lib موجود نخواهد بود و در صورتی که بخواهید در دیگر پروژه ها ازش استفاده کنید، به ارور unresolved external symbol برخواهید خورد.
اگر دلتون می خواد از template هاتون در پروژه های دیگه استفاده کنید، چند راه حل براش وجود داره:
ساده ترین کار، همینه که هم کد های declaration و هم پیاده سازی رو در یک فایل header انجام بدید. با این روش می تونید کتابخانه های header-only بسازید. معمولا کتابخانه هایی که template ارائه میدن از این روش استفاده می کنند:
می تونید definition ها و declration ها رو در دو فایل جدا بنویسید ولی definition ها رو در انتهای فایل هدر include کنید:
بعضی وقت ها فقط به یه سری نسخه های مشخصی از template هاتون نیاز دارید. در این موارد می تونید. از explicit template instantiation استفاده کنید:
و این هم فایل پیاده سازی:
حالا این روش به چه دردی می خوره؟
فرض کنید که شما می خواهید یک فایل .obj از روی همین فایل خروجی بگیرید و اون رو همراه با یه سری object file های دیگه به به لینکر بدید تا لینک کنه و خروجی تحویل بده اما مسئله اینجاست که قرار نیست از کد هایی که در این obj فایل نوشته شده به صورت مستقیم استفاده بشه بلکه قراره این کد ها به صورت غیر مستقیم و از طریق دیگر object file ها استفاده بشن، در اون صورت شما در حالت عادی به پیاده سازی های template هاتون دسترسی نخواهید داشت چرا که template هایی که استفاده نشده باشند اصلا در زمان کامپایل سورس در object file حاصل قرار نخواهند گرفت.
اگر تلاش کنید که بدون instantiation از template هایی که در یک translation unit دیگه تعریف شده اند در کدتون استفاده کنید، به ارور unresolved external symbol بر خواهید خورد.
معمولا این نوع روش در سناریو های زیر استفاده میشه:
خیلی خوب دوستان، به پایان این پست رسیدیم. امیدوارم براتون مفید بوده باشه. تا دفعه پست بعدی خداحافظ!