April 23, 2022

C++ RAII

CPP Basics

ניהול חיי המשאבים הוא נושא בסיסי שיש לשלוט בו בC++.
זאת מכיוון שC++ היא שפה בלתי מנוהלת.

ל-C++ יש Runtime שמבצע דברים מאוד בסיסיים.
אך אם ברצוננו למחוק זיכרון, לשחרר נעילת קובץ, לשחרר תהליכון רץ - עלינו לעשות זאת לבד.

RAII - Resource Acquisition Is Initialization

בתרגום חופשי ניתן לומר “השגת משאב הוא ביצירתו”.

RAII משתמש בטכניקה קשירת המשאב לחיי אובייקט.
כפי שידוע חיי אובייקט מוגדר ע”פ ה-Scope שלו.
Scope מוגדר בעזרת סוגריים מסולסלות { ... }.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct A { };

int main()
{
{ // a1 is created.
A a1;
} // a1 is out of scope.

{ // a2 is created.
A a2;
} // a2 is out of scope.
return 0;
}

יצירת מחלקה

כדי להדגים את היכולת ניצור מחלקה משלנו לניהול חיי זיכרון.

זיכרון מוקצה ע”י המילה השמורה new:

1
int* var = new int(5);

וכדי למחוק אותו נצטרך למחוק בצורה מפורשת:

1
delete var;  

זה יוצר מגוון בעיות:

  • יצירה ומחיקה מפורשת של זיכרון.
  • ניהול זיכרון בצורה ידנית.
  • כאשר מדובר במשאבים גדולים כך גם ניהול הזיכרון הופך למורכב יותר.
  • יכול ליצור דליפות זיכרון.

כדי להקל על יצירת ומחיקה ושימוש לא מפורש ביצירת הזיכרון נבצע נכמס את הפעולה הזו במחלקה.

הבנאי

הטכניקה משתמשת בבנאי כאתחול המשאב - אם יצירת המשאב נכשלת נזרקת שגיאה.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct EncapsulatePtr
{
EncapsulatePtr(int value)
{
try
{
// if new fails it will throw
Ptr = new int(value);
}
catch(std::bad_alloc& badAllocExc)
{
// Oops we failed
Ptr = nullptr;
}
}

int* Ptr;
};

כאשר נשתמש במשאב נוכל ליצור את המופע של המחלקה:

1
2
3
4
5
6
7
int main()
{
EncapsulatePtr myVal { 5 };
std::cout << *(myVal.Ptr);

// myVal goes out of scope and... memory leak! myVal.Ptr needs to be deleted.
}

מחיקת המשאב

כדי למחוק את המשאב נשתמש ב-dtor.
יש להימנע מזריקת שגיאות בעת מחיקת המשאב כדי לא להתעסק בזה בצורה מפורשת.
מחיקת משאבים אמורה להיות נקייה משגיאות ו”חלקה”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct EncapsulatePtr
{
EncapsulatePtr(int value)
{
try
{
// if new fails it will throw
Ptr = new int(value);
}
catch(std::bad_alloc& badAllocExc)
{
// Oops we failed
Ptr = nullptr;
}
}

~EncapsulatePtr()
{
if(Ptr != nullptr)
{
delete Ptr;
}
}

int* Ptr;
};

כעת השימוש בו הוא אוטומטי, לא צריך אפילו לחשוב על מחיקת המשאב:

1
2
3
4
5
6
int main()
{
EncapsulatePtr myVal { 5 };
std::cout << *myVal.Ptr;
// Out of scope - deleted.
}

דוגמאות ל-RAII

בספריית C++ הסטנדרטית קיימים דוגמאות לשימוש בקונספט:

std::string

הדוגמא הקלאסית היא מחרוזת.
מאחורי הקלעים קיימת מחרוזת מוקצת מראש למחרוזות קטנות - אך אם אנחנו מקצים מחרוזת ענקית על המחלקה להקצות זיכרון חדש.

1
2
3
4
5
6
7
8
int main()
{

// Stores AAA
std::string myString { "AAA" };
// Deletes - when out of scope.
return 0;
}

std::vector

ווקטור הוא לא השם הכי מוצלח למחלקה הזו - שם מוצלח יהיה list או dynamic list.

  • למרות שקיים גם std::list.
    וקטור כמו המחרוזת הוא מערך תווים סדרתי - ז”א הזיכרון שמוקצה בו הוא רשימה אחת של בייטים.
1
2
3
4
5
6
7
8
9
int main()
{
std::vector<int> myCev;
for(auto i=0 ; i<200 ; i++)
{
myVec.push_back(i);
}
return 0;
}

std::mutex

והדוגמא הכי קלאסית תהיה נעילות אוטומטיות:

1
std::mutex mutex;

נעילה תהיה ע”י קריאה למתודות lock ו-unlock.

מה לא לעשות:

1
2
3
4
5
6
7
8
9
10
11
12
13
void Dont() 
{
mutex.lock();

Do();

if(!IsOk())
{
// We dont release mutex!
return;
}
mutex.unlock();
}

מה כן לעשות:

1
2
3
4
5
6
7
8
9
10
11
12
void Do()
{
// RAII
std::lock_guard<std::mutex> lock(mutex);
Do();

if(!IsOk())
{
// Even if it's not ok we release the lock!
return;
}
}

Smart pointers

אי אפשר לדבר על RAII מבלי להזכיר את ה-Smart pointers.
במקום לנהל את המצביע לבד או לכתוב מחלקות בשביל זה יש לנו כמה מחלקות שעוזרות לנו:

  • std::unique_ptr - מצביע שיש אותו רק פעם אחת.
  • std::shared_ptr - מצביע השומר את כמות הייחסות למצביע.

זהו סוג של “מנגנון אוטומטי” לאיסוף זיכרון הנותן לנו שליטה על הבעלות על המצביע.

מהסיבה הזו יש קונבנציות המורות על שימוש במצביע נקי רק בשביל “להתייחס” לזיכרון ולא כבעלות לזיכרון.

1
2
3
4
5
6
7
8
{
std::shared_ptr<int> myPtr = std::make_shared<int>(5);
} //myPtr is deleted here

void MyFunction(int* ptr)
{
//ptr is a -non owning- pointer therefore we should not delete it!
}

טכניקת RAII פופולרית בשפה ויש לשלוט בה כדי לנהל משאבים בצורה חכמה.
השימוש במחלקה עוזרת בכך שניתן לממש עיצובים שונים למחלקות ולבנות היררכיות שייטפלו בכל המשאבים.
גם בירושה או הכלה.

זיכרון לא מנוהל זה זיכרון אבוד :)

תודה על הקריאה!

על הפוסט

הפוסט נכתב על ידי Ilya, רישיון על ידי CC BY-NC-ND 4.0.

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה

#Software#Cplusplus