לדלג לתוכן

1.7 - בעלות יחידה ותצוגות - פתרון

פתרון - הבעלים שומר אחסון והתצוגה שומרת חוזה

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

#include <cstddef>
#include <memory>
#include <span>
#include <utility>

class Buffer {
    std::unique_ptr<std::byte[]> data_;
    std::size_t size_ = 0;
public:
    explicit Buffer(std::size_t n)
        : data_(std::make_unique<std::byte[]>(n)), size_(n) {}

    Buffer(const Buffer&) = delete;
    Buffer& operator=(const Buffer&) = delete;

    Buffer(Buffer&& other) noexcept
        : data_(std::move(other.data_)),
          size_(std::exchange(other.size_, 0)) {}

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            data_ = std::move(other.data_);
            size_ = std::exchange(other.size_, 0);
        }
        return *this;
    }

    std::span<std::byte> bytes() noexcept {
        return {data_.get(), size_};
    }
    std::span<const std::byte> bytes() const noexcept {
        return {data_.get(), size_};
    }
};

std::size_t nonzero(std::span<const std::byte> input) {
    std::size_t result = 0;
    for (auto b : input) {
        if (b != std::byte{0}) ++result;
    }
    return result;
}

מימוש move מאפס גם את הגודל של המקור. אילו היינו מאפשרים move אוטומטי ללא בדיקת invariant, ה־unique_ptr היה עובר אבל המספר size היה נשאר במקור. קריאה ל־bytes על מקור כזה הייתה עלולה ליצור span עם pointer ריק וגודל חיובי.

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

הקוד הבא מצטרף לקובץ כבדיקה מלאה:

#include <cassert>

int main() {
    Buffer first{4};
    first.bytes()[1] = std::byte{7};
    assert(nonzero(first.bytes()) == 1);

    Buffer second{std::move(first)};
    assert(first.bytes().empty());
    assert(nonzero(second.bytes()) == 1);
}

מסירה והחזרה

אפשר לממש שינוי עם מסירת בעלות כך:

std::unique_ptr<int> update(std::unique_ptr<int> value) {
    if (value) *value = 12;
    return value;
}

הקריאה auto returned = update(std::move(original)); משאירה את original ריק ומחזירה בעלים חדש. לא נדרש release, מפני שכל מעבר מיוצג על ידי אובייקט בעלות.

תיקון התצוגה

אפשר להחזיר std::string לפי ערך, כך שלקורא יש בעלים עצמאי. לחלופין, פונקציה שמקבלת std::string_view input יכולה להחזיר input.substr(...), כל עוד הקורא שומר את תווי המקור בחיים ואינו מבצע פעולה שפוסלת את האחסון.

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