اشکان محمدی
اشکان محمدی
خواندن ۹ دقیقه·۳ سال پیش

همه آن چیزی که باید راجع به template های C++ بدانید

زبان C++ یک زبان همه منظوره و بسیار سریع هست و خیلی امکانات جالبی داره. یکی از این ویژگی ها template ها هستند. اگر قبلا با زبان های static typed مثل java و #C کار کرده باشید، حتما با مفهوم Generic ها آشنا هستید و می دونید که به چه منظور استفاده میشن.

در C++ هم template ها مفهومی شبیه به generic ها دارند اما تفاوت های اساسی بین اینها وجود داره که در ادامه به آنها اشاره خواهیم کرد.

استفاده template ها در توابع

فرض کنید که تابعی داریم که قراره دو تا عدد رو با هم جمع کنه. از اونجایی که C++ سیستم static typed دارد، برای هر نوع خاصی از عدد باید یک overload از روی تابع اصلی ایجاد کنیم:

اگر به تابع بالا نگاهی بیندازیم، الگوی زیر رو مشاهده می کنیم:

خوب اگر تنها چیزی که قراره تغییر کنه نوع مقادیر ورودی و مقدار خروجی هست، چرا نتونیم این دو تابع رو یکی کنیم؟ درست حدس زدید. template ها بهمون دقیقا همین قابلیت رو میدهند. حالا اگر تابع add رو به روش زیر بازنویسی کنیم، می تونیم هر نوع عددی رو دریافت، جمع و به عنوان نتیجه برگردونیم:

اگر با Macro ها در C++ آشنا باشید، می دونید که از macro ها برای جایگذاری متن هنگام کامپایل استفاده میشه. template ها خیلی به macro ها شبیه هستند. template ها یه سری آرگومان میگیرند که بهشون میگن template arguments و بر اساس اون آرگومان ها یک تابع جدید، یک Struct جدید یا یک class جدید در زمان کامپایل ایجاد می کنند.

فراخوانی template function ها

در مثال بالا، اگر بخواهید تابع add را در جایی از کد هاتون فراخوانی کنید باید به این شکل این کار رو انجام بدید:

add<int>(10, 20);

پارامتر int که بین <> قرار داره، مشخص می کنه که تابع در ادامه چه نوع داده ای رو قبول می کنه. البته در C++17 ویژگی به نام implicit template argument deduction اضافه شده که بر اساس اون، درصورتی که نوع template argument ها از روی پارامتر های داده شده به تابع قابل تشخیص باشه، دیگه نیازی به مشخص کردن صریح template argument ها بین <> نیست.

مثلا در C++17 می تونید تابع add رو مثل هر تابع معمولی دیگه ای فراخوانی کنید و کامپایلر C++ خودش تشخیص میده که نوع داده استفاده شده چی هست:

add(10, 20);

استفاده از template ها در کلاس ها و struct ها

همچنین می تونید از template ها در کلاس ها به این صورت استفاده کنید


تنها نکته ای که می خوام در کد بالا بهش اشاره کنم عبارت های using هستش. اگر قبلا با زبان C کار کرده باشید حتما می دونید typedef چی هست. عبارت های using بالا هم دقیقا کار typedef رو انجام میدن.

مثلا اگر بنویسیم:

using Number_Type = int;

و بعد متغیری از نوع Number_Type تعریف کنیم، دقیقا مثل این هست که یک int تعریف کرده باشیم:

Number_Type n1 = 45; int n1 = 45; // Both are equivalent

عبارت هایی که با کلیدواژه using ساخته میشن، در موقعیت های مختلف استفاده های مختلفی دارند:

  • استفاده از member های یک namespace دیگر در scope برنامه
  • استفاده از متد های کلاس والد در فرزند
  • ساخت type alias که بهش اشاره کردیم.

من قرار نیست راجع به این ها در این مقاله توضیح بدم چون از موضوع بحث ما خارجه. اما اگر می خواهید راجع به اونها بدونید می تونید به این مقاله مراجعه کنید

استفاده در struct ها

خوب در struct ها هم روند کار دقیقا به همین صورت هست:

می توانید داده های literal به عنوان آرگومان به template ها بدهید

شاید فکر کنید که template ها فقط قادر به گرفتن نوع داده به عنوان آرگومان هستند. ولی کاملا در اشتباه هستید. چون شما می تونید از constant ها و literal ها نیز به عنوان آرگومان استفاده کنید. بزارید بهتون با یک مثال ساده نشون بدم:

همونطور که میبینید، آرگومان size از نوع int هست. حالا میتونید به این شکل از روی DataContainer یک obj ایجاد کنید:

می تونید یک literal یا یک constant به عنوان size به template بدید ولی استفاده از یک متغیر مجاز نیست. چون مقدار size باید در زمان کامپایل مشخص و در زمان اجرا ثابت باشه که متغیر ها همچین ضمانتی ندارند.

می توانید با template ها metaprogramming انجام بدهید

با template ها میشه الگوریتمی نوشت که در زمان کامپایل اجرا بشه و اینطوری کمی از بار پردازش زمان اجرا میشه کم کرد.

حالا برای محاسبه فاکتوریل عدد 5 میشه همچین چیزی نوشت:

unsigned int result = factorial<5>::value

مبحث metaprogramming مقوله خیلی گسترده ای هست. برای اطلاعات بیشتر می تونید به این لینک مراجعه کنید.

ویژه سازی template ها یا template specialization

شما می تونید رفتار خاصی برای نوع خاصی از داده ها در نظر بگیرید. مثلا فرض کنید که تابعی به نام sort داریم. اگر ورودی تابع عدد باشه، اعداد رو باید از کوچیک به بزرگ مرتب کنه. اگر ورودی string باشه، باید به ترتیب حروف الفبا مرتبش کنه. اگر ورودی یک تاریخ باشه، باید اون رو بر اساس روز، ماه و سال مرتب کنه:

اگر توجه کرده باشید، در مثالی که کمی قبل تر برای محاسبه factorial زدیم، از template specialization نیز استفاده کردیم که زمانی که مقدار آرگومان 0 بود، value برابر 1 باشه و دیگه در یک حلقه بی نهایت نیوفتیم. اگر می خواهید راجع به این مبحث بیشتر بدونید، می تونید به این لینک مراجعه کنید.

تمپلیت های variadic

تا به حال از خودتون پرسیده اید که چطور توابعی مانند 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 رو بسازه. به طور کلی دو راه برای این کار وجود داره:

  • وقتی که شما از template ای که تعریف کردید، در همون translation unit (منظور همون فایل .cpp و header هاشه که template رو در اون تعریف کردید | همون فایلی که قراره به object file تبدیل بشه) استفاده کنید. اینطوری به صورت غیر مستقیم به کامپایلر می فهمونید که در زمان کامپایل یک نسخه از روی template ایجاد کنه.
  • از روش explicit template instantiation استفاده کنید. یعنی بدون اینکه در جایی از کد استفاده کنید، مستقیما و صریح به کامپایلر بگید باید یک نسخه از template شما رو ایجاد کنه. این روش زمانی به درد می خوره که می خواهید از template هاتون در translation unit های مختلف استفاده کنید. در ادامه در این باره بیشتر بحث خواهیم کرد

در روش اول شما به آسونی در جایی از کد، تابع خود را فراخوانی می کنید و کامپایلر overload های مختلفی از تابع مورد نظر رو ایجاد می کنه:

ولی در روش دوم باید به شکل زیر به کامپایلر مستقیما اعلام کنید که باید نسخه های مورد نیاز از روی add ایجاد بشه. اینکه این روش به چه دردی می خوره و چرا باید از همچین چیزی استفاده کنیم در بخش بعدی مطلب ذکر شده پس فعلا زیاد فکر خودتون رو درگیرش نکنید:

و برای کلاس ها اینطور باید عمل کنیم:

استفاده از template ها در translation unit های مختلف

در C++ به هر فایلcpp که قراره تبدیل به یک object file بشه میگن translation unit. اگر یادتون باشه، گفته شد که template ها واقعی نیستند. پس اگر بدون استفاده از اونها فایل هامون رو compile کنیم، یعنی مثلا یک فایل lib برای استفاده در دیگر پروژه هامون بسازیم، با یک مشکل بزرگ مواجه میشیم. چون کد های پیاده سازی template ها در فایل lib موجود نخواهد بود و در صورتی که بخواهید در دیگر پروژه ها ازش استفاده کنید، به ارور unresolved external symbol برخواهید خورد.

اگر دلتون می خواد از template هاتون در پروژه های دیگه استفاده کنید، چند راه حل براش وجود داره:

  1. نوشتن کد declaration و definition ها، با هم در یک فایل header.
  2. نوشتن definition ها در یک فایل cpp و include کردن اون فایل در انتهای فایل header
  3. استفاده از explicit template instantiation

روش اول:

ساده ترین کار، همینه که هم کد های declaration و هم پیاده سازی رو در یک فایل header انجام بدید. با این روش می تونید کتابخانه های header-only بسازید. معمولا کتابخانه هایی که template ارائه میدن از این روش استفاده می کنند:

some_header.hpp file
some_header.hpp file

روش دوم:

می تونید definition ها و declration ها رو در دو فایل جدا بنویسید ولی definition ها رو در انتهای فایل هدر include کنید:

header.hpp
header.hpp
impl.cpp
impl.cpp

روش سوم:

بعضی وقت ها فقط به یه سری نسخه های مشخصی از template هاتون نیاز دارید. در این موارد می تونید. از explicit template instantiation استفاده کنید:

و این هم فایل پیاده سازی:

حالا این روش به چه دردی می خوره؟

فرض کنید که شما می خواهید یک فایل .obj از روی همین فایل خروجی بگیرید و اون رو همراه با یه سری object file های دیگه به به لینکر بدید تا لینک کنه و خروجی تحویل بده اما مسئله اینجاست که قرار نیست از کد هایی که در این obj فایل نوشته شده به صورت مستقیم استفاده بشه بلکه قراره این کد ها به صورت غیر مستقیم و از طریق دیگر object file ها استفاده بشن، در اون صورت شما در حالت عادی به پیاده سازی های template هاتون دسترسی نخواهید داشت چرا که template هایی که استفاده نشده باشند اصلا در زمان کامپایل سورس در object file حاصل قرار نخواهند گرفت.

اگر تلاش کنید که بدون instantiation از template هایی که در یک translation unit دیگه تعریف شده اند در کدتون استفاده کنید، به ارور unresolved external symbol بر خواهید خورد.

معمولا این نوع روش در سناریو های زیر استفاده میشه:

  • پیاده سازی های template ها در فایل های جدایی هستند که بعدا قراره ازشون خروجی lib. بگیرید و ازشون در پروژه های دیگه استفاده کنید.
  • ایجاد template ها در زمان کامپایل پروژه اصلی به صرفه نیست و از طرفی شما فقط به چند نسخه خاص از template تون نیاز دارید و برای بهینه سازی تایم کامپایل پروژه اصلی، کل نسخه هایی که از قبل نیاز دارید رو می خواهید یک بار برای همیشه کامپایل کنید و فقط از همون نسخه های کامپایل شده استفاده کنید. اینطوری هر بار نیاز به آپلود نیست.



خیلی خوب دوستان، به پایان این پست رسیدیم. امیدوارم براتون مفید بوده باشه. تا دفعه پست بعدی خداحافظ!











templatesmetaprogramminggenericscبرنامه نویسی
یه برنامه نویس ساده که از تجربیات و آموخته هاش می نویسه
شاید از این پست‌ها خوشتان بیاید