اشاره گر های هوشمند: shared_ptr

توی پست قبل درباره اینکه چرا باید از اشاره گر های هوشمند استفاده کنیم حرف زدم و کلاس std::unique_ptr رو بررسی سطحی کردیم. توی این پست قراره درباره نوع دیگه ای از اشاره گر های هوشمند بنویسم که برعکس unique_ptr به ما اجازه میده چند شئ بتونن یک حافظه یکسان رو مدیریت کنن.

ادامه خواندن “اشاره گر های هوشمند: shared_ptr”

معرفی std::string_view

یکی از ویژگی های جالبی که به سی++ ۱۷ اضافه شده، std::string_view هست که برای کار با رشته های ثابت خیلی زیاد به کار آدم میاد.

کد پایین رو ببینید:

#include <iostream>
#include <string>
 
int main()
{
  char text[]{ "hello" };
  std::string str{ text };
  std::string more{ str };
 
  std::cout << text << ' ' << str << ' ' << more << '\n';
 
  return 0;
}

توی این کد، با اینکه تنها کاری که ما کردیم چاپ کردن رشته بوده و رشته هم ثابت بوده، اما برای همین استفاده ساده ۴ بار از اون رشته کپی گرفته شده.

یک بار رشته ثابت توی کد باینری قرار گرفته. بعد کپی شده توی استک (برای text)، و دو بار هم std::string ها کپیش کردن برای ساختن شئ خودشون.

سی++ ۱۷ قابلیتی به اسم string_view اضافه کرده که برای همینکار درست شده 🙂

کد پایین رو ببینید:

#include <iostream>
#include <string_view>
 
int main()
{
  std::string_view text{ "hello" }; // view the text "hello", which is stored in the binary
  std::string_view str{ text }; // view of the same "hello"
  std::string_view more{ str }; // view of the same "hello"
 
  std::cout << text << ' ' << str << ' ' << more << '\n';
 
  return 0;
}

توی این کد رشته ما فقط یکبار کپی شده (و اونم توی باینری) و بقیه متغییر های ما به نوعی دارن به همون رشته اصلی اشاره می‌کنن.

و نکته مثبتش؟ اینکه خیلی از توابع مربوط به std::string رو هم ساپورت می‌کنه.

تغییر دادن رشته

اساسا این کلاس برای رشته های read-only ساخته شده و عملا نمیتونه تغییری توی رشته اصلی انجام بده. همونطور که از اسمش معلومه فقط یک view از رشته هست.

یک پنجره رو فرض کنید، شما از طریق پنجره به منظره بیرون نگاه می‌کنید. وقتی پرده رو می‌کشید دید شما محدود میشه اما در منظره بیرون تغییری ایجاد نمیشه. اما اگر منظره بیرون تغییر کنه، دید شما هم تغییر میکنه.

این مثال دقیقا برای این کلاس کاربرد داره. اگر رشته اصلی ای که string_view باهاش ساخته شده تغییر بکنه، رشتهٔ ای که string_view داره نمایش میده هم تغییر می‌کنه.

اگر رشته ای که string_view باهاش ساخته شده از بین بره، دیگه نمیشه از اون string_view استفاده کرد و فراخوانی کردنش یک undefined behaviour هست.

مثال:

#include <iostream>
#include <string_view>
 
int main()
{
  char arr[]{ "Gold" };
  std::string_view str{ arr };
 
  std::cout << str << '\n'; // Gold
 
  // Change 'd' to 'f' in arr
  arr[3] = 'f';
 
  std::cout << str << '\n'; // Golf
 
  return 0;
}

در پایان

  • از این کلاس برای رشته هایی که قرار نیست تغییر بکنن استفاده کنیم.
  • برای رشته هایی که قراره تغییر بکنن از همون std::string استفاده کنیم.

فصل ۲۲ دایتل: نکات اضافه

توی این فصل درمورد struct ها، bit field ها، اوپراتور های بیتی (bitwise) و توابع مربوط به دستکاری رشته های در سی صحبت شده.

البته دو مورد آخر رو من بلد بودم بنابراین توی این پست دربارشون چیزی نمی‌نویسم. راستی، این آخرین فصل کتاب فیزیکیه 🙂

structure ها

نکته جالب اینکه struct ها تقریبا همون کلاس ها هستند و تنها تفاوت‌شون اینه که اعضای یک struct بر خلاف کلاس، به صورت پیش‌فرض public هست و همچنین نوع ارث بری به صورت پیش‌فرض public هست(که در کلاس ها private هست).

typedef و using

همونطور که از قبل می‌دونستیم، از typedef برای تعریف کردن alias برای انواع داده هامون استفاده می‌کردیم. مثلا:

typdef char* string;

توی سی++ ۱۱ قابلیتی اضافه شده که میشه اینکار رو با استفاده از using انجام داد و عبارت پایین برابر با همون typedef عمل می‌کنه:

using string = char*;

Binary Literals ها

در سی++‌۱۴ میتونیم از Binary Literal ها استفاده بکنیم.

برای اینکار کافیه که پشت عبارتمون، کاراکتر 0b یا 0B رو قرار بدیم:

const unsigned binary{0b1000000'00000000'00000000'00000000};

Bit Field ها

‌سی++‌ این امکان رو به ما میده که تعیین کنیم یک عضو خاص(از نوع int یا enum) یک کلاس یا یک struct چقدر از حافظه رو اشغال بکنه(تعداد بیت ها). به عضوی که این قابلیت براش استفاده شده باشه میگن Bit Field.

struct BitCard {
unsigned face : 4;
unsigned suit : 2;
unsigned color : 1;
};

با توجه به مثال بالا، اندازهٔ حافظه ای(تعداد بیت ها) که میخوایم عضو ما داشته باشه رو با یک «:» جدا می‌کنیم.

اصطلاحا به اندازه ای که تعیین می‌کنیم میگن width of the bit field.

unnamed bit field

سی++ این امکان رو به ما میده که عضوی تعریف بکنیم که به عنوان padding استفاده بشه. این عضو هیچ اسمی نداره و صرفا به تعداد بیت ای که مشخص می‌کنیم، حافظه رو رزرو می‌کنه و اون بخش از حافظه قابل استفاده نیست.

struct Example {
unsigned a : 13;
unsigned : 3; // align to next storage-unit boundary
unsigned b : 4;
};

توی مثال بالا تعیین کردیم که ۳ بیت از حافظه رزرو بشه.

این رزرو کردن به یک شکل دیگه هم میتونه نوشته بشه و اون هم اینکه تعداد بیت های unnamed رو برابر با صفر قرار بدیم.

در این حالت خود کامپایلر میاد و هرچقدر که فضا در اون واحد حافظه باقی مونده رو رزرو می‌کنه. عضو بعدی ای که تعریف می‌کنیم در یک واحد دیگه قرار می‌گیره.

struct Example {
unsigned a : 13;
unsigned : 0; // align to next storage-unit boundary
unsigned b : 4;
};

نکات بیت فیلد ها

  • نباید با استفاده از اوپراتور & سعی کنیم که آدرس یک بیت فیلد رو بگیریم.
  • استفاده از بیت فیلد ها باعث میشه که کامپایلر کد کندتری رو تولید بکنه. بخاطر اینکه محاسبه های بیشتری برای پیدا کردن آدرس های حافظه باید انجام بده.

پایان

خب این فصل هم به پایان رسید و رسما فصل های موجود در کتاب pdf «آموزش برنامه نویسی سی++ دایتل» تموم شد. از این به بعد نکات اضافی ای که از منابع دیگه یاد میگیرم رو در پست های جدا می‌نویسم. کتاب جالبی بود 🙂

فصل ۲۱ دایتل: رشته ها

توی این فصل در مبحث رشته ها در سی++ بیشتر عمیق میشیم. درواقع کلاس string از STL رو بررسی می‌کنیم.

پست کوتاهیه چون بیشتر چیزها رو بلد بودم 🙂

تابع compare

این تابع جالبیه که باهاش میتونیم بخش خاصی از یک رشته رو با بخش خاصی از یک رشته دیگه مقایسه کنیم.

اینطوری:

string str1{"This is string"};
string str2{"oh This string"};
if (str1.compare(0, 3, str2, 3, 5)) {
// do something
}

تابع replace و نکته‌ش

تابع replace میاد و از مکانی که براش مشخص می‌کنیم میگرده و مقدار مشخص شده رو پیدا می‌کنه و رشتهٔ جایگزین رو بجاش می‌ذاره.

نکته‌ش اینه که اگر مثلا شما دنبال یک رشته بگردید که ۲ کاراکتر داره و بخواین این زیر رشته رو با یک رشته که ۴ کاراکتر داره جایگزین کنید، ۲ کاراکتر بعد از رشته مورد جستجوی ما از بین میره.

الآن حوصله نوشتن مثال ندارم. امیدوارم گنگ نگفته باشم 🙂

برای اینکه به این مشکل برنخوریم بهتره که اول موقعیت رخداد رشته مورد جستجو رو با استفاده از تابع find پیدا بکنیم و بعد با استفاده از تابع insert رشته مورد نظرمون رو بذاریم بجاش.

تبدیل رشته به عدد

توابعی که برای اینکار وجود دارن اینا هستن:

توابع مربوط به تبدیل رشته به عدد

تابع هایی که می‌بینیم درواقع ۳ تا ورودی دارن(به جز تابع های مربوط به اعداد اعشاری که ورودی سوم رو ندارن) که دوتای آخرشون default دارن.

  • ورودی اول: رشته ای که میخوایم به عدد تبدیل بشه
  • یک اشاره گر به size_t برای ذخیره کردن اولین اندیسی که این تابع قادر به تبدیلش نبوده
  • یک عدد صحیح بین ۲ تا ۳۶ که مبنای عدد رو مشخص می‌کنه.

به عنوان مثال فرض کنیم str یک رشته است که مقدار “123” در اون ذخیره شده. اینطور میشه به int تبدیلش کرد:

int convertedInt = stoi(str, nullptr, 10);

دوتا آرگومان آخر دلخواه هستند و میشه اونهارو ننوشت.

پایان

خیلی خوبه که بیشتر نکات این چندفصل آخر رو بلد بودم و دارم سریع پیش میرم 🙂 باعث میشه اعتماد به نفس بهتری داشته باشم.

فصل ۱۹ دایتل: نکاتی درباره پیاده سازی ساختمان داده ها

این فصل از دایتل درباره پیاده سازی ساختمان داده های مرسوم مثل لیست پیوندی، صف، استک و درخت دودویی با استفاده از تمپلیت ها بود. چون من این چیز ها رو از قبل بلد بودم بنابراین این پست بسیار کوتاه بود چون همونطور که میدونیم، من فقط چیزهایی رو می‌نویسم که به نظرم جدیدن.

کلاس Self-Referential

کلاس های خود ارجاعی به کلاسی گفته میشه که یک data member داشته باشه که به یک شئ از جنس خود کلاس اشاره میکنه.

نام های وابسته و غیر وابسته (dependent names vs non-dependent)

به کد زیر توجه کنید:

توابعی که توی این کد استفاده شده در کلاس List تعریف شدن.

همونطور که می‌بینید، کلاس Stack از کلاس List که یک class template هست ارث بری کرده و همونطور که می‌دونیم، تمپلیت ها درواقع همون function overloading هستند که وقتی type رو براشون مشخص می‌کنیم، کامپایلر با اون type مشخص شده کد رو تولید می‌کنه.

خط ۱۳ و خط ۱۸ نمونهٔ اسم های وابسته هستن.

یعنی چی؟ یعنی اینکه تا وقتی نوع STACKTYPE مشخص نشده باشه، کد کلاس List تولید نشده و در نتیجه توابع ذکر شده هم تولید نشدن و کامپایلر این رو می‌فهمه(چون می‌بینه در ورودی‌شون یه STACKTYPE دارن).

خط ۲۳ و ۲۸ نمونه اسم های غیر وابسته یا non-dependent هستن.

این تابع ها(مثل isEmpty و print) چون هیچ ورودی ای ندارن، پس وابسته به type نیستند و کامپایلر وقتی به خطی می‌رسه که داره تابع print رو صدا می‌زنه،‌ گمون می‌کنه که این یک تابع معمولیه و کدش موجوده(درحالی که این تابع جزئی از کلاس List هست و تا مشخص شدن STACKTYPE کدی براش تولید نمیشه).

این مسئله باعث میشه ارور بوجود بیاد.

برای اینکه به کامپایلر بفهمونیم این توابع نباید در زمان دیده شدن resolve بشن و resolve شدن‌شون رو باید به بعد از تولید کد template موکول کرد، از کلمه this استفاده می‌کنیم.

پایان

فصل بعدی درباره الگوریتم های مرتب سازی (sort) و جستجو (search) هست.

فصل ۱۶ دایتل: الگوریتم های موجود در کتابخانه استاندارد

در این فصل قراره که یاد بگیریم چطور با استفاده از iterator ها و algorithm(الگوریتم) های موجود در کتابخانه استاندارد سی++ یا همون STL کارهامون رو پیش ببریم. یاد میگیریم که توابع لاندا چی هستن و چطور ازشون استفاده بکنیم، اشاره گر به تابع چیه و چطور میشه ازش استفاده کرد و چیز های دیگه.

نکاتی درباره پیمایش‌گر ها (Iterators)

اینکه یه کانتینر از چه iterator هایی پشتیبانی میکنه مشخص کنندهٔ اینه که از چه الگوریتم هایی میشه برای این کانتینر استفاده کرد. به عنوان مثال کانتینر های vector و array. این دو کانتینر از random-access iterator پشتیبانی میکنن(وقتی از این نوع پشتیبانی میکنن یعنی از بقیه انواع پیمایش کننده ها هم پشتیبانی میکنن) و این یعنی همهٔ الگوریتم های موجود رو میشه براشون استفاده کرد. البته نکته اینجاست که الگوریتم هایی که سایز کانتینر رو تغییر میدن برای array قابل استفاده نیستن. بنابراین مهم نیست که کانتینر چیه، اگه اون کانتینر، حداقل نوع iterator مورد نیاز برای یه الگوریتم رو ساپورت بکنه، میشه از اون الگوریتم براش استفاده کرد.

باطل شدن پیمایش ها (iterator invalidation)

ایتریتور ها درواقع یک اشاره گر کپسوله شدن هستند که به عناصر کانتینر اشاره میکنن بنابراین ممکنه در صورت بروز یک سری تغییرات در کانتینر (که به کانتینر بستگی داره)، این اشاره گر اعتبارشو از دست بده و باطل بشه. پروسه invalidate شدن اشاره گر ها، رفرنس ها و ایتریتور ها در بخش 23 استاندارد سی++ موجوده و ما اینجا فقط خلاصه ای از اونها رو بررسی می‌کنیم.

اضافه کردن یه عنصر به کانتینر:

  • در vector ها، اگر اضافه کردن عنصر ما باعث بشه که وکتور اقدام به درخواست فضای بیشتر و در نتیجه reallocate شدن بکنه، تمام iterator هایی که مربوط به این وکتور بودن باطل میشن. در غیر این صورت، هر iterator ای که به فضای بین مکان عنصری که تازه اضافه شده و مکان آخرین عنصر موجود اشاره میکنه باطل میشه.
  • در deque ها، همه iterator ها باطل میشن.
  • در list, forward_list و ordered associative container ها هیچ تغییری در iterator ها بوجود نمیاد
  • در unordered associative container ها تنها اگر عمل reallocation انجام بگیره، همه iterator ها باطل میشن.

حذف کردن یک عنصر از کانتینر باعث میشه iterator ای که به اون عنصر اشاره می‌کنه باطل بشه. علاوه بر اون:

  • در وکتور ها از اونجایی که عنصر حذف شده تا آخرین عنصر هر پیمایش‌گری وجود داشته باشه غیر فعال میشه
  • در deque ها اگر حذفی که رخ داده در جایی غیر از ابتدا یا انتهای کانتینر باشه باعث میشه کل پیمایشگر ها باطل بشن.

توابع لاندا

خیلی از الگوریتم های موجود در STL میتونن یک تابع‌‌‌‌‌‌ رو به عنوان ورودی خودشون داشته باشن. همونطور که قبلا می‌دونیم، اسم یک تابع به صورت ضمنی یک اشاره گر به ابتدای کد اون تابع هست. اما یک راه هم وجود داره که تابع خودمون رو منحصرا برای اون الگوریتمی که داریم ازش استفاده میکنیم بنویسیم و اون رو دقیقا کنار بقیه آرگومان ها بنویسیم! برای اینکار یک مفهوم به اسم توابع لاندا به کارمون میاد که در واقع توابعی هستند که اسم ندارند و اصطلاحا بهشون میگن anonymous function.

برای اینکه دقیق تر متوجه بشیم، یه مثال رو بررسی می‌کنیم:

یکی از الگوریتم های جالبی که در STL وجود داره، اسمش for_each هست. این تابع میاد بازه ای از یک کانتینر رو میگیره و تابعی که ما بهش میدیم رو برای تمام عناصر موجود در اون بازه اجرا میکنه. بهتره که کد رو ببینیم:

مثالی برای نشون دادن for_each و تابع لاندا. دیگه خود کد رو ننوشتم چون عکسش قشنگ تر درمیومد

خب همونطور که می‌بینیم، تابع for_each برای دوتا آرگومان اولش دوتا iterator میگیره و آرگومان سومش یه تابع رو به عنوان ورودی دریافت می‌کنه. توی این مثال ما دوتا for_each زدیم که اولی میاد و صرفا عناصر رو ضربدر ۲ میکنه و چاپ می‌کنه و دومی میاد جمع همهٔ عنصر هارو محاسبه می‌کنه. چجوریش رو میگم حالا.

سینتکس توابع لاندا

خب آرگومان اول و دوم که واضحن. میمونه آرگومان سوم که یه تابع لانداست. سینتکس توابع لاندا این شکلی ان:

[introducer](input arguments) {function  body}

خب همونطور که می‌بینید توابع لاندا با یک [] شروع میشن که اصطلاحا بهشون میگن lambda introducer. بقیه‌ش تقریبا مثل تابع معمولیه و لیست پارامتر های ورودی میاد و در ادامه بدنه تابع قرار داره.

توابع لاندا میتونن به متغییر های محلی(local) جایی که دارن داخلش تعریف میشن دسترسی داشته باشن. مثلا توی مثال بالا تابع های لاندای ما میتونن به متغییر هایی که داخل main تعریف شده دسترسی داشته باشن. اینجاست که lambda introducer به کار میاد. درواقع lambda introducer به ما اجازه میده که مشخص کنیم از کدوم متغییر های موجود میخوایم استفاده کنیم. به اینکار اصطلاحا میگن capture کردن متغییر ها.

توی اولین for_each می‌بینیم که lambda introducer خالیه و این یعنی تابع لاندای ما نمیخواد از هیچ متغییری استفاده کنه. و توی دومی ما این رو می‌بینیم: [&sum] و این یعنی رفرنسی از متغییر sum رو در دسترس تابع قرار میده که ازش استفاده بکنه. دلیل اینکه از رفرنس استفاده شده هم اینه که بتونیم متغییر اصلی که داخل main قرار داره رو modify کنیم.

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

تا الآن تابع های لاندای ما هیچ مقدار بازگشتی ای نداشتن و بنابراین به صورت پیشفرض مقدار بازگشتیشون به عنوان void مشخص میشد. اما اگر داخل تابع لاندامون یه return داشته باشیم نیاز داریم که نوع مقدار بازگشتی رو از طریق سینتکس trailing return type مشخص کنیم.

اینطوریه :

-> type

که اگر بخوام توی کد نشون بدم اینطوری میشه:

[] () -> int {return 2}

همونطور که می‌بینیم، جاش بین لیست پارامتر ها و بدنه تابع‌ست.

الگوریتم ها

این بخش از فصل تعداد زیادی الگوریتم رو توضیح داده ولی من فقط اونایی که به نظرم بدرد بخور تر یا جالب تر میان رو اینجا می‌نویسم.

mismatch

وظیفهٔ این تابع اینه که بین دوتا کانتینر بگرده و اونجایی که دوتا خونه متناظر باهم یک مقدار مساوی نداشته باشن، می‌ایسته و اطلاعات اون مکان رو (به شکل یک pair از iterator های هردو کانتینر) بهمون برمیگردونه. این تابع ۴ تا ورودی داره(اونی که توی کتاب نشون داده اینطوریه) که دوتای اول بازه رو برای کانتینر اول مشخص میکنن و دوتای دوم بازه رو برای کانتینر دوم مشخص میکنن.

inserter ها

بیاین تابع merge رو باهم ببینیم:

vector<int> a1{1, 2, 3};
vector<int> a2{4, 5, 6};
vector<int> result;
std::merge(a1.begin(), a1.end(), a2.begin(), a2.end(), result.begin());

توی مثال بالا، وکتور result باید حتما به اندازه ۶ تا خونه جا داشته باشه تا a1 و a2 داخلش ذخیره بشن. بنابراین باید قبل از اجرای تابع merge، باید تخصیص حافظه صورت بگیره.

اما زمانی هست که ما نمی‌خوایم از قبل حافظه ای تخصیص بدیم و میخوایم یک کلاسی مثل وکتور، خودش اینکار رو به ازای اضافه شدن عناصر جدید انجام بده. اینجاست که inserter ها (از هدر iterator) به کمک ما میان. مثال بالا اگر اجرا بشه به مشکل میخوره چراکه result به اندازه کافی جا نداره. حالا با استفاده از inserter این مشکل رو برطرف می‌کنیم:

vector<int> a1{1, 2, 3};
vector<int> a2{4, 5, 6};
vector<int> result;
std::merge(a1.begin(), a1.end(), a2.begin(), a2.end(), back_inserter(result));

تابع back_inserter درواقع میاد و به ازای هر عنصری که میخواد به result اضافه بشه، تابع push_back مربوط به کانتینر result رو صدا می‌زنه. به همین راحتی 🙂

Function Object ها

همونطور که می‌دونیم، بسیاری از الگوریتم های موجود در کتابخونه استاندارد میتونن یک تابع رو به عنوان آرگومان آخرشون بگیرن. تا اینجا دیدیم که این تابع میتونه یک function pointer یا یک تابع لاندا (lambda function) باشه. کلاس هایی که میتونن توابع لاندا یا اشاره گر به توابع رو به عنوان ورودی بگیرن، میتونن یک نوع دیگه از تابع رو هم دریافت کنن که اسمش function object هست. function object درواقع یک شئ از کلاسی هست که اوپراتور پرانتزش overload شده. یعنی ما در member function های کلاس، یک تابع به اسم operator() تعریف کردیم.

اشیاء ای که از این کلاس ما ساخته میشن میتونن بجای تابع لاندا یا اشاره گر به تابع استفاده بشن.(درواقع خود توابع لاندا توسط کامپایلر به یک اشاره گر به تابع یا function object تبدیل میشن تا بشه روشون بهینه سازی انجام داد).

در بیشتر جاها(و نه همه جا) میشه بجای function object از تابع لاندا یا اشاره گر به تابع استفاده کرد.

از طریق هدر <functional> میتونیم به function object های از پیش تعریف شده STL دسترسی داشته باشیم که خیلی هم کاربردی و خفنن. تابع less<T> که توی مثال های بالا(بخش set و …) دیدیم جزئی از function object های موجود در STL عه.

مزایای function object ها

اولین تفاوتش با لاندا و امثالهم اینه که از اونجایی که عضوی از یک کلاسه، کامپایلر راحت تر میتونه بهینه سازی هایی مثل inline کردن رو انجام بده.

دومین تفاوت که یک نقطه قوت محسوب میشه، قابلیت استفاده از data member های کلاس هست.

پایان

این فصل هم تموم شد. مثل فصل های قبلی با تاخیر اما برخلاف فصل های قبلی، تاخیرش زیاد نبود! فصل بعدی توی مدیریت استثنا ها و خطا ها عمیق میشیم.

نکات جدیدی که از فصل ۱۵ دایتل یاد گرفتم – کتابخانه استاندارد

توی این فصل سه بخش از کتابخانه استاندارد سی++ که بهش STL هم میگن رو بررسی می‌کنیم. container ها، iterator ها و algorithm ها. فصل بسیار مهمیه چراکه این کتابخونه بسیاری از کار های مارو راحت تر می‌کنه و اگر خوب بلد باشیم ازش استفاده کنیم، دهن خودمون رو برای پیاده سازی کردن خیلی از چیز ها صاف نمی‌کنیم.

کانتینر ها

کانتینر ها یک ساختمان داده ای هستند که تقریبا میشه همه نوع داده ای رو توشون ذخیره کرد. درکل سه نوع کانتینر داریم که به شکل زیر دسته بندی میشن:

  • first class containers
  • container adapters
  • near containers

یک نوع دسته بندی دیگه هم وجود داره که کانتینر هارو به ۴ بخش تقسیم می‌کنه:

  • Sequence containers
  • Ordered associative containers
  • unordered associative containers
  • container adapters

بخش sequence containers و associative containers درواقع به عنوان first class container در نظر گرفته میشن.

container adapter در اصل همون first class container ها هستن که عملیات هاشون محدود شده. این کانتینر شامل استک، صف و … هست.

یک نوع کانتینر دیگه هم داریم که بهشون میگن near containers. دلیل اینکه اسمشون رو به این شکل انتخاب کردن اینه که این کانتینر ها بعضی از قابلیت های first-class container هارو دارن و بخش دیگه ایشون رو ندارن. مثالی که میشه از این کانتینر ها زد؟ built-in array ها، کلاس های مربوط به bitset و valarray ها(که برای انجام عملیات های سریع ریاضی روی وکتور ها* بکار میره) و …

*اون وکتور، با vector ای که توی کانتینر ها داریم فرق داره.

توی جدول زیر میتونیم لیست کانتینر ها و ویژگی‌هاشون رو ببینیم که خیلی جالبه:

نکاتی درباره پرفورمنس کانتینر ها

  • اضافه/حذف کردن به/از آخر vector ها سریعه. اما اضافه/حذف کردن به/از اول و یا وسط vector ها به صرفه نیست
  • اگر نیاز داریم که صورت مفرط به ابتدا/انتها کانتینرمون المان اضافه(یا حذف) بکنیم بهتره از deque (تلفظ میشه دِکْ) استفاده بکنیم چرا که عملیات هاش در ابتدا و انتهای کانتینر سریعن
  • در آخر اگه نیاز داریم که در وسط کانتینر هم چیزی رو حذف کنیم یا اضافه کنیم، بهتره از list استفاده بکنیم.

Sequence Container ها:

وکتور ها (vector)

وکتور کانتینری هست که از خونه های متوالی حافظه استفاده میکنه. در واقع در لایه های زیرین وکتور میاد و با یک اندازه ثابت یک آرایه تخصیص میده. بعد از پر شدن آرایه، یک آرایه با اندازهٔ بیشتر از حافظه میگیره و اطلاعات آرایه قبلی رو در آرایه جدید کپی میکنه(یا move میکنه) و آرایه قبلی رو حذف می‌کنه. در واقع این قابلیت آرایه بودن این امکان رو بهش میده که بشه به صورت آنی به المان های وکتور دسترسی پیدا کرد.

اگر می‌دونیم که حدودا قراره چه مقدار داده به وکتور اضافه کنیم، بهتره با استفاده از توابع resize یا reserve اون حافظه رو برای وکتور بگیریم تا از تخصیص و حذف پی در پی حافظه جلوگیری کنیم. C++11: shrink_to_fit

اینکه چطور یک وکتور اقدام به افزایش حافظه میکنه بستگی به پیاده سازی داره و ممکنه توی کامپایلر های مختلف، نتیجهٔ متفاوتی داشته باشه. در حالت کلی این یک time-space tradeoff هست.

فرق بین clear و erase

فرق این دو این هست که تابع clear کل اعضای وکتور رو پاک میکنه اما erase این قابلیت رو داره که تک عضو و یا یک رنج از عضو ها رو از وکتور پاک کنه.

لیست ها (list)

لیست که درواقع یک لیست دو پیوندی هست (doubly linked list) اجازهٔ اضافه و حذف کردن سریع در هر جای کانتینر رو میده اما در حالت کلی اگر بیشتر عملیات هامون قراره در دو انتهای کانتینر باشه، بهتره که از دِک (deque) استفاده کنیم.

تابع unique

این تابع عناصر تکراری یه کانتینر رو حذف می‌کنه. البته برای اینکه درست کار کنه کانتینر ما از قبل باید سورت شده باشه تا عناصر تکراری کنار هم قرار بگیرن.

دِک ها (deque)

کلاس دک درواقع نکات مثبت وکتور و لیست رو توی خودش جمع کرده. کلمهٔ deque کوتاه شدهٔ double-ended queue هست. این کلاس قابلیت دسترسی سریع و مستقیم به عناصر داره و همچنین عملیات هایی که در دو سمت انتهایی این کانتینر انجام میشن سریعن.

از اونجایی که از random-access iterator ها ساپورت میکنه بنابراین همهٔ الگوریتم های کتابخونه استاندارد میتونن روی این کلاس اعمال بشن.

در حالت کلی دک سربار بیشتری از وکتور داره و همچنین حذف و اضافه کردن در وسط دک ها بهینه تر از وکتوره(همچنان کند تر از لیست ها)

حالا که یک دید کلی از Sequence container ها داریم، به Associative Container ها می‌پردازیم:

این کانتینر ها قابلیت این رو به ما میدن که با استفاده از یک کلید بتونیم به صورت مستقیم به مقادیر مورد نظرمون دسترسی داشته باشیم. ۸ نوع کلاس وجود داره که ۴ تای اول کلید هارو به صورت مرتب ذخیره میکنن و ۴ تای دوم، ترتیب کلید ها براشون مهم نیست.

multiset, set, multimap, map
unordered_multiset, unordered_set, unordered_multimap, unordered_map

کلاس های set و multiset به ما یه مجموعه ای از مقادیر رو میدن که خود اون مقدار ها، کلید هم هستن. فرق اصلی این دو کلاس اینه که کلاس set اجازه نمیده مقادیر تکراری به مجموعه اضافه بشن اما کلاس multiset این اجازه رو میده.

کلاس های map و multimap یه مجموعه به ما میدن که هر کلید، به یک مقدار وصله. فرقشون هم اینه که در کلاس map نمیشه یک کلید چند مقدار متفاوت داشته باشه اما این امر توی multimap امکان پذیره.

کلاس multiset

این کلاس که از هدر <set> میتونیم بهش دسترسی داشته باشیم به ما قابلیت ذخیره سازی و بازیابی سریع مقادیر رو میده همچنین قابلیت این رو داره مقادیر تکراری رو ذخیره کنه. این کلاس برای مرتب کردن عناصرش از چیزی به نام comparator function object استفاده می‌کنه که توی فصل بعد بهش می‌پردازیم. اگر ترتیب مقدار ها مهم نیست بهتره که از unordered_multiset استفاده بکنیم چراکه سربار کمتری داره(هدر<unordered_set>) . مثال برای استفاده از multiset:

std::multiset <int, less<int>> values;

که در اینجا اون less<int> یک comparator function object هست(اختیاری) و باعث میشه مقادیر ما به صورت صعودی مرتب بشن.

کلاس set

این کلاس تنها فرقی که با multiset داره اینه که عناصر تکراری رو ignore میکنه و درواقع همهٔ عناصر موجود در اون، یکتا هستن.

کلاس multimap

این نوع از associative container که از طریق هدر map میشه بهش دسترسی داشت به ما این قابلیت رو میده که مقادیر رو به صورت «جفت» (pair) ذخیره کنیم. یعنی اینکه به ازای هر مقدار، یک کلید وجود داره. اینکه کلید ها به چه ترتیبی مرتب بشن رو میشه از طریق comparator function ها تعیین کرد. کلاس multimap اجازه میده که کلید های تکراری داشته باشیم یعنی یک کلید میتونه چندین مقدار داشته باشه که بهش میگن one-to-many relationship و احتمالا بخاطر همینه که برای این کلاس random-access iterator نذاشتن.

کلاس map

این کلاس شبیه multimap عه تنها با این تفاوت که امکان وجود کلید تکراری نیست. یعنی هر کلید فقط به یک مقدار اشاره میکنه (one-to-one mapping) و همچنین این قابلیت وجود داره که به مقدار هر کلید به صورت آنی دسترسی داشته باشیم. ( با استفاده از اوپراتور []).

هردو کلاس قبلی که گفته شد دارای یک نسخه غیر مرتب هم هستند که سربار کمتری داره و برای استفاده ازشون کافیه یه unordered_ پشت اسم کلاس بذاریم.

نکاتی درباره Container Adapter ها:

این کانتینر ها در اصل همون first class container ها هستند که عملیات هاشون محدود شده و از iterator پشتیبانی نمی‌کنن. بنابراین لایه زیرین کلاس های این کانتینر از کلاس های first class container ها تشکیل میشه.

  • کلاس stack که یک استک رو میسازه، به صورت پیشفرض از deque استفاده می‌کنه.
  • کلاس queue که یک صف رو میسازه، به صورت پیشفرض از deque استفاده می‌کنه.
  • کلاس priority_qeueu که یک صف اولویت دار رو میسازه(صفی که مقادیر داخلش معمولا با استفاده از تکنیک heap، مرتب شده‌ند)، به صورت پیش‌فرض از کلاس vector به عنوان لایه زیرین خودش استفاده می‌کنه comparator function هم داره.

پیمایش کننده ها (Iterators)

ایتریتور ها چیزی شبیه به پوینتر ها هستند که قابلیت های بیشتری دارن و برای دسترسی و تغییر المان های یک کانتینر بکار میرن. مکانیسم دسترسی و پیمایش در یک کانتینر رو کپسوله میکنن و این به الگوریتم ها اجازه میده که بدون وابستگی به پیاده سازی لایه کانتینر، بتونن کار خودشون رو انجام بدن.

چند تابع داریم که میتونن برامون یک iterator برای یک کانتینر و یا حتی یک آرایه بسازن.

توابع begin و end که یک اشاره گر به اعضاء کانتینر میسازن( از سی++ ۱۱ به بعد میتونن یک پوینتر از built-in array ها بسازن حتی) و توابع cbeing, cend که یک ایتریتور const میسازن و rbegin, rend, crbegin, crend که قابلیت پیمایش برعکس روی یک آرایه رو میدن(از سی++ ۱۴ به بعد).

الگوریتم ها

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

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

نکات جدیدی که از فصل ۱۲ سی++ دایتل یاد گرفتم – چندریختی

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

چند ریختی به ما اجازه میده که برنامه های گسترده ای بنویسیم و درواقع بتونیم برنامه نویسی general داشته باشیم بجای اینکه مجبور بشیم به صورت خاص برنامه بنویسیم(الآن براش مثالی توی ذهنم ندارم که کوتاه باشه).

ادامه خواندن “نکات جدیدی که از فصل ۱۲ سی++ دایتل یاد گرفتم – چندریختی”

نکات جدیدی که از فصل ۱۱ سی++ دایتل یاد گرفتم – ارث بری

این فصل از دایتل درباره‌ی ویژگی ارث‌بری توی سی++ بود. نکاتی که برام جدیده رو توی این پست نوشتم.

تفاوت is-a relationship و has-a relationship

وقتی یک کلاسی(A) از یک کلاس دیگه(B) ارث بری می‌کنه، میگیم که رابطه‌شون از نوع is-a relationship هست. یعنی کلاس A نوعی از کلاس B عه؛ مثالش؟ «خودرو» و «وسیله نقلیه». خودرو از وسیله‌ی نقلیه مشتق شده(نمیدونم این تعبیرِ مشتق شدن درسته یا نه) و تمام ویژگی های یک وسیله‌ی نقلیه رو داره. درواقع، «خودرو یک وسیله‌ی نقلیه‌ست».

اما وقتی یک کلاس در data-member های خود یک شئ از یک کلاس دیگه رو داره(composition) میگیم رابطه‌شون has-a relationship هست. توی مثال خودرو، میشه فرمون خودرو رو مثال زد که خودش یک شئ هست و عضوی از خودرو محسوب میشه(نه نوعی از خودرو!).

Constructor

در حالت عادی، توابع سازنده(constructor)، نابودکننده(destructor) و اوپراتور تخصیص (operator= ) از کلاس والد ارث برده نمیشن.

وقتی یک شئ از کلاس فرزند(B) ساخته میشه اگه کلاس والد(A) دارای default constructor باشه، بدون اینکه کانستراکتور والد رو صراحتا صدا بزنیم، خودش به صورت ضمنی اجرا میشه. اما اگه کلاس A دارای default constructor نباشه باید حتما خودمون کانستراکتور کلاس A رو توی کانستراکتور کلاس B صدا بزنیم.

دسترسی به عناصر private والد

کلاس فرزند نمیتونه به طور مستقیم به عناصر private کلاس والد دسترسی داشته باشه. برای اینکه بشه اینکار رو انجام داد، اون عناصر مورد نظر توی کلاس والد باید به عنوان protected تعریف شده باشن یا اینکه کلاس فرزند از طریق توابع public کلاس والد اقدام به دسترسی گرفتن به عناصر پرایوت بکنه.

با استفاده از protected access specifier به دلیل اینکه میتونید به صورت مستقیم به عناصر مورد دسترسی داشته باشید، برنامه‌تون کمی سریعتر میشه. در مقابل استفاده از توابع public باعث میشه برنامه‌تون کمی کند تر باشه.

اما این روش(protected) مشکلاتی هم داره:

اول اینکه ممکنه داده های invalid بهشون تخصیص داده بشه. یعنی از اونجایی که دسترسی به صورت مستقیم و بدون اعتبارسنجی به اون‌ها وجود داره، ممکنه یک داده‌ی اشتباهی بهشون تخصیص داده بشه.

دوم اینکه برنامه رو شکننده (اصطلاحا fragile) میکنه. یعنی کلاس فرزند جای اینکه بر پایه خدماتی(service) باشه که کلاس والد ارائه میده، بر پایه‌ی پیاده سازی (implementation) کلاس والد توسعه پیدا میکنه. مشکلش چیه؟ از نظر مهندسی نرم افزار بهتره که تغییرات رو localize بکنیم. یعنی برنامه طوری نباشه که اگر اسم یکی از عناصر رو توی کلاس والد تغییر دادیم، مجبور بشیم تمام جاهایی از کلاس فرزندمون که به اون عضو رفرنس دادیم رو تغییر بدیم و استفاده از protected پتانسیل بوجود اومدن چنین مشکلی رو ایجاد می‌کنه.

مگه نگفتی استفاده از توابع public برنامه رو کندتر می‌کنه؟

چرا. اما بهتره که ما اصول مهندسی نرم افزار رو رعایت کنیم و بهینه کردن کد(optimization) توی این مورد رو به دست کامپایلر بسپاریم. چون خیلی وقتا کامپایلر خودش بهینه سازی های خوبی انجام میده(مثلا ممکنه توابع set و get رو به حالت inline دربیاره). به قول معروف، Do not second-guess the compiler .

فرق بین ارث بری public و بقیه

خود کتاب یه جدول خیلی خوب رسم کرده که من عکسش رو میذارم. به نظرم خوندن همون کافیه 🙂

جدولی که توضیح میده هرکدوم از انواع ارث بری public, private, protected چه ویژگی هایی دارن.

ارث بردن constructor

بالاتر گفتم که در حالت عادی کانستراکتور از کلاس والد به ارث برده نمیشه. اما توی C++ 11 ویژگی ای اضافه شده که اجازه میده کانستراکتور والد توسط کلاس فرزند به ارث برده بشه.

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

using BaseClass :: BaseClass ;

که BaseClass اسم کلاس والد هست. با اینکار، کامپایلر به ازای کانستراکتور های موجود توی کلاس والد، یه کانستراکتور توی کلاس فرزند میسازه که به طور خودکار تابع متناظرش توی کلاس والد رو صدا بزنه و data-member های کلاس فرزند رو default initialize بکنه.

چندتا نکته برای این کار وجود داره:

  • کانستراکتور های default, copy, move ارث بری نمیشن.
  • اگر توی کلاس فرزند کانستراکتوری باشه که ورودی هاش با ورودی های یک کانستراکتور توی کلاس والد یکسان باشه، اون کانستراکتور به ارث برده نمیشه.
  • اگه کانستراکتور دارای default argument باشه، به همون صورت به ارث نمیرسه بلکه به صورت چند کانستراکتور overload شده به ارث میرسه.
  • اگه یه کانستراکتور توی کلاس والد حذف بشه، توی کلاس فرزند هم حذف میشه.

در آخر

فصل بعدی درباره‌ی چند ریختی یا همون polymorphism عه که دانش ابتدایی ما برای برنامه نویسی شئ‌گرا رو کامل میکنه.

نکاتِ جدیدی که از فصل ۱۰ سی++ دایتل یادگرفتم – نکاتی پراکنده

خب فصل ۱۰ ام از کتاب برنامه نویسی سی++ دایتل رو تموم کردم. اینجا خلاصه ای از چیزای جدیدی که یادگرفتم رو می‌نویسم.

string-object literal

توی سی++ ۱۴ میشه با اضافه کردن یک s به انتهای لیترال رشته‌ای‌مون(بعد از دابل کوتیشن، نه قبلش) اون رشته رو تبدیل به یه شئ از کلاس string کرد. مثال:

"Hello, World"s

Operator Overloading؛ به عنوان عضو کلاس یا نه؟

با اینکه من قبلا این بخش هارو توی کتاب C How To Program خونده بودم اما چون یادداشت نکرده بودم و خیلی وقت هم شده بود که حوصله‌ی برنامه نویسی رو نداشتم، یادم رفته بود.

مسئله اینه که چه وقتی باید تابع اوپراتوری که overload میشه رو جزئی از member function ها بذاریم و چه وقتی جزئی از non-member function. ساده‌ست، تنها زمانی میتونیم تابع اوپراتور رو به عنوان عضوی از کلاس قرار بدیم که شئِ کلاس ما به عنوان پارامتر در سمت چپ قرار بگیره. این کد رو ببینید:

istream& operator>>(istream& input, MyClass& object);

وقتی مینویسیم cin >> myobject درواقع انگار تابع رو به شکل operator>>(input, myobject) فراخوانی کردیم.

حالا از اونجایی که شئ ما توی پارامتر سمت چپ قرار نمیگیره، نمیتونیم این اوپراتور رو داخل کلاس overload کنیم و اگر اینکارو انجام دادیم، باید اینطوری بنویسیمش: myobject << cin

از طرف دیگه، برای اینکه بتونیم اوپراتور هامون رو به صورت commutative(جابجایی پذیر) تعریف کنیم، به دلایلی که بالاتر توضیح داده شد باید حداقل یکبار اوپراتورمون رو به عنوان non-member function تعریف کنیم.

نکته درباره‌ی Dynamic Allocation در عضو های کلاس

وقتی یک حافظه ای رو به صورت پویا برای یکی از اعضای کلاس‌مون (data-member) در نظر می‌گیریم باید حواسمون باشه که
Default memberwise assignment و Default copy constructor رو به حال خودشون رها نکنیم چون مشکلاتی مثل double free رو بوجود میارن. پس یادمون باشه که کپی کانستراکتور خودمون + علامت مساوی(=) خودمون رو تعریف کنیم.

حذف یه تابع از کلاس

یه تابع از کلاس (function member) رو میشه حذف کرد. قبلا برای اینکار، اون تابع رو جزء بخش private کلاس قرار می‌دادند اما الان به عنوان مثال میشه اینطوری نوشت:

const Array* operator=(const Array*) = delete;

کاربردش چیه؟ معمولا برای غیرفعال کردن توابعی که توسط کامپایلر به صورت خودکار ساخته میشن (auto generate)، مثل سازنده‌ی پیشفرض(Default constructor)، کپی کانستراکتور، اوپراتور تخصیص (=) و … استفاده می‌شن.

تعریف شئ از کلاس با initializer list

برای اینکه بتونیم یه شئ از کلاسمون رو با initializer list بسازیم، باید یه کانستراکتور داشته باشیم که ورودی‌ش از نوع initializer_listباشه. مثال:

MyClass::MyClass(initializer_list list);

تبدیل انواع مختلف به کلاس و بالعکس

ما میتونیم اشیاء ای که از کلاسمون میسازیم رو با استفاده از Conversion Constructor ها و Conversion Operator ها به انواع دیگه ای تبدیل کنیم.

برای اینکه یه کانستراکتور تبدیل کننده داشته باشیم نیاز داریم که کانستراکتور ما بتونه با یک آرگومان صدا زده بشه.(این مسئله برای کپی کانستراکتور ها صادق نیست) مثال :

MyClass:MyClass(int a);

حالا با استفاده از این تابع میشه هم به صورت ضمنی و هم به صورت مستقیم یک int رو به شئ ای از کلاس خودمون تبدیل کنیم.

برای تعریف یک Conversion Operator میتونیم اینطوری عمل کنیم:

MyClass::operator string() const;

حالا اگر فرض کنیم a اسم شئ ما باشه، وقتی بنویسیم static_cast<string> (a) درواقع کامپایلر تابع a.operator string() رو صدا میزنه.

چرا توابع سازنده‌مون رو به شکل explicit تعریف کنیم؟

توابع سازنده ای که میتونن فقط با یک آرگومان صدا زده بشن ممکنه توسط کامپایلر اشتباها به عنوان Conversion Constructor تلقی بشن(چون این تابع ها هم با یک ورودی صدا زده میشن) و به صورت ضمنی عمل cast رو انجام بدن. درکل ما برای اینکه از تبدیل ضمنی چه توی کانستراکتور(اونایی که میتونن با یه آرگومان صدا زده بشن) و چه توی اوپراتور های تبدیلی( conversion operator) جلوگیری کنیم، اون توابع رو به صورت explicit تعریف می‌کنیم.

فصل بعدی درباره‌ی ارث‌بری هست 🙂