לדלג לתוכן

פרויקט - ספריית בעלות - פתרון

פתרון - מפרידים בעלות מפרטי הסגירה

נשתמש בתבנית קטנה כדי להראות שהלוגיקה של move ושל reset זהה, אבל ערך הכשל ופונקציית הניקוי שונים. התבנית בפרויקט הזה מצומצמת; פרק הספריות ילמד כיצד להגדיר constraints וממשק גנרי רחב יותר.

הקוד הבא הוא תוכנית מלאה ל־MSVC x64 עם Windows SDK. סדר ה־headers חשוב מפני ש־Winsock2 צריך להופיע לפני header של Windows:

#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <windows.h>

#include <cassert>
#include <cstddef>
#include <cstdio>
#include <memory>
#include <span>
#include <stdexcept>
#include <utility>

#pragma comment(lib, "Ws2_32.lib")

struct EventTraits {
    using value_type = HANDLE;
    static value_type invalid() noexcept { return nullptr; }
    static void close(value_type value) noexcept {
        CloseHandle(value);
    }
};

struct SocketTraits {
    using value_type = SOCKET;
    static value_type invalid() noexcept { return INVALID_SOCKET; }
    static void close(value_type value) noexcept {
        closesocket(value);
    }
};

template<class Traits>
class UniqueResource {
public:
    using value_type = typename Traits::value_type;
private:
    value_type value_ = Traits::invalid();
public:
    UniqueResource() noexcept = default;

    explicit UniqueResource(value_type value) noexcept
        : value_(value) {}

    ~UniqueResource() noexcept { reset(); }

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

    UniqueResource(UniqueResource&& other) noexcept
        : value_(other.release()) {}

    UniqueResource& operator=(UniqueResource&& other) noexcept {
        if (this != &other) reset(other.release());
        return *this;
    }

    value_type get() const noexcept { return value_; }

    explicit operator bool() const noexcept {
        return value_ != Traits::invalid();
    }

    value_type release() noexcept {
        return std::exchange(value_, Traits::invalid());
    }

    void reset(value_type replacement = Traits::invalid()) noexcept {
        if (replacement == value_) return;
        auto old = std::exchange(value_, replacement);
        if (old != Traits::invalid()) Traits::close(old);
    }
};

using Event = UniqueResource<EventTraits>;
using Socket = UniqueResource<SocketTraits>;

class Winsock {
public:
    Winsock() {
        WSADATA data{};
        int result = WSAStartup(MAKEWORD(2, 2), &data);
        if (result != 0) throw std::runtime_error("WSAStartup failed");
    }
    ~Winsock() noexcept { WSACleanup(); }
    Winsock(const Winsock&) = delete;
    Winsock& operator=(const Winsock&) = delete;
};

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_};
    }
};

Event make_event() {
    Event event{CreateEventW(nullptr, TRUE, FALSE, nullptr)};
    if (!event) throw std::runtime_error("CreateEventW failed");
    return event;
}

void demonstrate_failure() {
    Event event = make_event();
    throw std::runtime_error("injected failure");
}

int main() {
    try {
        Winsock runtime;
        Event first = make_event();
        Event second = make_event();
        HANDLE original = first.get();

        second = std::move(first);
        assert(!first);
        assert(second.get() == original);
        second = std::move(second);
        assert(second.get() == original);

        Event adopted{second.release()};
        assert(!second);
        assert(adopted.get() == original);

        Socket socket_owner{socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)};
        if (!socket_owner) throw std::runtime_error("socket failed");

        Buffer input{16};
        input.bytes()[0] = std::byte{7};
        Buffer output{std::move(input)};
        assert(input.bytes().empty());
        assert(output.bytes()[0] == std::byte{7});

        try {
            demonstrate_failure();
        } catch (const std::runtime_error&) {
            std::puts("expected injected failure");
        }

        std::puts("ownership checks passed");
    } catch (const std::exception& error) {
        std::fprintf(stderr, "%s\n", error.what());
        return 1;
    }
}

הפקודה לבנייה היא:

cl /nologo /std:c++20 /EHsc /W4 /Zi /Od owner_test.cpp /Fe:owner_test.exe

למה הסדר חשוב

האובייקט Winsock נבנה לפני ה־Socket ולכן מושמד אחריו. כיוון ההשמדה ההפוך מבטיח שה־runtime של Winsock עדיין פעיל כאשר closesocket נקראת. אפשר לשבור את החוזה אם מעבירים socket לבעלים שחי מעבר ל־runtime, ולכן הספרייה עדיין צריכה לתעד את התלות הזאת.

ה־Event משתמש ב־null כמצב ריק. ה־Socket משתמש ב־INVALID_SOCKET. הקוד אינו מניח ששניהם בני אותו גודל או שיש להם אותו sentinel; התבנית מקבלת את ההחלטה מה־Traits.

מה קורה בהשמה

הפעולה release של המקור מחזירה את המשאב ומרוקנת את המקור. הפעולה reset של היעד מחליפה את המשאב ושומרת את הישן במשתנה מקומי, כדי לסגור אותו בדיוק פעם אחת.

התנאי replacement equals value מגדיר כאן ש־reset עם אותו ערך הוא פעולה ריקה. הוא אינו מתיר לשני בעלים שונים להחזיק אותו משאב; מצב כזה עדיין מפר את תנאי האימוץ. ה־constructor מאמץ משאב שהקורא חייב למסור לו בבעלות בלעדית.

איך בודקים סגירה אחת

אפשר להחליף במבחן יחידה את ה־Traits ב־policy של מספרים מלאכותיים. ה־policy שומר מונה לכל מספר ומוודא שאף מספר אינו נסגר פעמיים. זהו מבחן של לוגיקת הבעלות, שאינו תלוי במצב מערכת ההפעלה.

בדיקת מערכת נפרדת יוצרת eventים ו־socketים אמיתיים ובוחנת את מסלול הסגירה. צריך להבחין בין שתי הבדיקות: mock שמראה close יחיד אינו מוכיח שהשתמשנו בפונקציית OS המתאימה, ותצפית על מספר handles לבדה אינה מכסה את כל מקרי move.

כשל בנייה וגבולות הפתרון

ב־demonstrate_failure נוצרת בעלות לפני הזריקה. בזמן unwinding ה־Event מושמד וה־handle נסגר. הקוד אינו דורש catch שמכיר את שם המשתנה או סוג המשאב כדי לנקות אותו.

ההודעות שהדוגמה זורקת הן קצרות לצורך הפרויקט. במוצר נרצה לשמור גם GetLastError או WSAGetLastError בזמן הנכון, ולהציג הקשר פעולה. גם תוצאות CloseHandle ו־closesocket אינן מדווחות מה־destructor כאן; אם נדרשת התחייבות לסגירה נצפית, מוסיפים פעולה מפורשת עם חוזה תוצאה.

הפתרון אינו מאפשר להעתיק את הבעלים, ואינו שומר תצוגות לאחר שהמקור נעלם. זאת התשתית שנשתמש בה בהמשך עבור מיפויי קובץ, בקשות I/O ואובייקטי מערכת נוספים.