לדלג לתוכן

1.8 - בעלות משותפת וחיים תלויים - פתרון

פתרון - ניווט להורה אינו בעלות עליו

התוכנית הבאה יוצרת שורש וילד עם קשר חלש בחזרה:

#include <cstdio>
#include <memory>
#include <vector>

struct Node {
    const char* name;
    std::weak_ptr<Node> parent;
    std::vector<std::shared_ptr<Node>> children;

    explicit Node(const char* n) : name(n) {}
    ~Node() { std::printf("destroy %s\n", name); }
};

int main() {
    auto root = std::make_shared<Node>("root");
    auto child = std::make_shared<Node>("child");
    child->parent = root;
    root->children.push_back(child);
    child.reset();

    root.reset();
    std::puts("finished");
}

ההורה מחזיק את הילדים בחיים, אבל הילד אינו מאריך את חיי ההורה. כאשר root מתאפס, גוף ה־destructor שלו רץ ולאחריו מושמד ה־vector של הילדים. הילד האחרון מאבד בעלות חזקה ומושמד גם הוא.

אם משתנה חיצוני עדיין היה מחזיק את child, הילד היה נשאר חי אחרי היעלמות root. במקרה כזה lock על parent היה נכשל, וזה מצב שהממשק צריך לדעת לייצג. קשר חלש אינו מבטיח שההורה תמיד קיים.

אירוע שהגיע אחרי הסגירה

הקוד הבא הוא תוכנית עצמאית:

#include <cstdio>
#include <functional>
#include <memory>

struct Session {
    int events = 0;
};

int main() {
    auto session = std::make_shared<Session>();
    std::weak_ptr<Session> weak = session;

    std::function<void()> callback = [weak] {
        if (auto owner = weak.lock()) {
            ++owner->events;
            std::printf("event=%d\n", owner->events);
        } else {
            std::puts("ignored");
        }
    };

    callback();
    session.reset();
    callback();
}

הקריאה הראשונה מדפיסה אירוע אחד, והשנייה מתעלמת. הבעלים המקומי שנוצר מתוך lock מחזיק את Session בחיים רק בזמן גוף העבודה. אין כאן dereference של pointer בלי בעלות תקפה.

חיי אובייקט לעומת מקביליות

העתקת shared_ptr קיים משתתפת באותו control block. בניית shared_ptr חדש מתוך pointer שמתקבל ב־get אינה יודעת על הבעלות הקודמת, ולכן יוצרת אחריות נפרדת לאותה כתובת. שני מנגנוני הניקוי עלולים להשמיד את אותו אובייקט פעמיים.

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

כאשר שני threadים משנים events יחד, שניהם יכולים להחזיק בעלות תקינה ועדיין ליצור data race. צריך mutex, atomic מתאים או העברת העבודה לבעלים יחיד של המצב. הבחירה תלויה בחוזה הפעולה ולא בסוג המצביע שמחזיק את האובייקט.